UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

859 lines (843 loc) 41.9 kB
import _extends from "@babel/runtime/helpers/extends"; /** * @jsxRuntime classic * @jsx jsx */ // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766 import { jsx } from '@emotion/react'; import isEqual from 'lodash/isEqual'; import memoizeOne from 'memoize-one'; import { TableSortOrder as SortOrder } from '@atlaskit/custom-steps'; import { CHANGE_ALIGNMENT_REASON, INPUT_METHOD } from '@atlaskit/editor-common/analytics'; import { DropdownMenuExtensionItems } from '@atlaskit/editor-common/floating-toolbar'; import { addColumnAfter, addRowAfter, backspace, tooltip } from '@atlaskit/editor-common/keymaps'; import commonMessages, { tableMessages as messages } from '@atlaskit/editor-common/messages'; import { isNestedTablesSupported, isSelectionTableNestedInTable } from '@atlaskit/editor-common/nesting'; import { getTableContainerWidth } from '@atlaskit/editor-common/node-width'; import { areToolbarFlagsEnabled } from '@atlaskit/editor-common/toolbar-flag-check'; import { DEFAULT_BORDER_COLOR, cellBackgroundColorPalette } from '@atlaskit/editor-common/ui-color'; import { closestElement, getChildrenInfo, getNodeName, isReferencedSource } from '@atlaskit/editor-common/utils'; import { findParentDomRefOfType } from '@atlaskit/editor-prosemirror/utils'; import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles'; import { shortcutStyle } from '@atlaskit/editor-shared-styles/shortcut'; import { Rect, TableMap } from '@atlaskit/editor-tables/table-map'; import { findCellRectClosestToPos, findTable, getSelectionRect, isSelectionType, splitCell } from '@atlaskit/editor-tables/utils'; import AlignImageCenterIcon from '@atlaskit/icon/core/align-image-center'; import AlignImageLeftIcon from '@atlaskit/icon/core/align-image-left'; import CopyIcon from '@atlaskit/icon/core/copy'; import CustomizeIcon from '@atlaskit/icon/core/customize'; import DeleteIcon from '@atlaskit/icon/core/delete'; import TableColumnsDistributeIcon from '@atlaskit/icon/core/table-columns-distribute'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { clearHoverSelection, hoverColumns, hoverMergedCells, hoverRows, hoverTable, removeDescendantNodes } from '../pm-plugins/commands'; import { deleteColumnsWithAnalytics, deleteRowsWithAnalytics, deleteTableWithAnalytics, distributeColumnsWidthsWithAnalytics, emptyMultipleCellsWithAnalytics, insertColumnWithAnalytics, insertRowWithAnalytics, mergeCellsWithAnalytics, setColorWithAnalytics, setTableAlignmentWithAnalytics, sortColumnWithAnalytics, splitCellWithAnalytics, toggleFixedColumnWidthsOptionAnalytics, toggleHeaderColumnWithAnalytics, toggleHeaderRowWithAnalytics, toggleNumberColumnWithAnalytics, wrapTableInExpandWithAnalytics } from '../pm-plugins/commands/commands-with-analytics'; import { getPluginState as getDragDropPluginState } from '../pm-plugins/drag-and-drop/plugin-factory'; import { getPluginState } from '../pm-plugins/plugin-factory'; import { pluginKey as tableResizingPluginKey } from '../pm-plugins/table-resizing/plugin-key'; import { getStaticTableScalingPercent } from '../pm-plugins/table-resizing/utils/misc'; import { getNewResizeStateFromSelectedColumns } from '../pm-plugins/table-resizing/utils/resize-state'; import { pluginKey as tableWidthPluginKey } from '../pm-plugins/table-width'; import { canMergeCells } from '../pm-plugins/transforms/merge'; import { normaliseAlignment } from '../pm-plugins/utils/alignment'; import { isTableNested } from '../pm-plugins/utils/nodes'; import { getSelectedColumnIndexes, getSelectedRowIndexes } from '../pm-plugins/utils/selection'; import { getMergedCellsPositions } from '../pm-plugins/utils/table'; import { TableCssClassName } from '../types'; import { FloatingAlignmentButtons } from './FloatingAlignmentButtons/FloatingAlignmentButtons'; export const getToolbarMenuConfig = (config, state, { formatMessage }, editorAnalyticsAPI, isTableScalingWithFixedColumnWidthsOptionShown = false, areTableColumnWidthsFixed = false) => { const optionItem = 'item-checkbox'; const options = [{ id: 'editor.table.lockColumnWidths', title: formatMessage(messages.lockColumnWidths), onClick: toggleFixedColumnWidthsOptionAnalytics(editorAnalyticsAPI, INPUT_METHOD.FLOATING_TB), selected: areTableColumnWidthsFixed, hidden: !isTableScalingWithFixedColumnWidthsOptionShown, domItemOptions: { type: optionItem } }, { id: 'editor.table.headerRow', title: formatMessage(messages.headerRow), onClick: toggleHeaderRowWithAnalytics(editorAnalyticsAPI), selected: state.isHeaderRowEnabled, hidden: !config.allowHeaderRow, domItemOptions: { type: optionItem } }, { id: 'editor.table.headerColumn', title: formatMessage(messages.headerColumn), onClick: toggleHeaderColumnWithAnalytics(editorAnalyticsAPI), selected: state.isHeaderColumnEnabled, hidden: !config.allowHeaderColumn, domItemOptions: { type: optionItem } }, { id: 'editor.table.numberedColumn', title: formatMessage(messages.numberedRows), onClick: toggleNumberColumnWithAnalytics(editorAnalyticsAPI), selected: state.isNumberColumnEnabled, hidden: !config.allowNumberColumn, domItemOptions: { type: optionItem } }, { id: 'editor.table.collapseTable', title: formatMessage(messages.collapseTable), onClick: wrapTableInExpandWithAnalytics(editorAnalyticsAPI), selected: !!state.isTableCollapsed, disabled: !state.canCollapseTable, hidden: !config.allowCollapse, domItemOptions: { type: optionItem } }]; const tableOptionsDropdownWidth = isTableScalingWithFixedColumnWidthsOptionShown ? 192 : undefined; if (state.isDragAndDropEnabled) { return { id: 'editor.table.tableOptions', type: 'dropdown', testId: 'table_options', iconBefore: CustomizeIcon, title: formatMessage(messages.tableOptions), hidden: options.every(option => option.hidden), options, dropdownWidth: tableOptionsDropdownWidth }; } else { return { id: 'editor.table.tableOptions', type: 'dropdown', testId: 'table_options', title: formatMessage(messages.tableOptions), hidden: options.every(option => option.hidden), options, dropdownWidth: tableOptionsDropdownWidth }; } }; // Added these options for mobile. Mobile bridge translates this menu and // relay it to the native mobile. Native mobile displays the menu // with native widgets. It's enabled via a plugin config. export const getToolbarCellOptionsConfig = (editorState, editorView, initialSelectionRect, { formatMessage }, getEditorContainerWidth, api, editorAnalyticsAPI, isTableScalingEnabled = false, isTableFixedColumnWidthsOptionEnabled = false, shouldUseIncreasedScalingPercent = false, isCommentEditor = false, isLimitedModeEnabled = false) => { var _pluginState$pluginCo, _pluginState$pluginCo2; const { top, bottom, right, left } = initialSelectionRect; const numberOfColumns = right - left; const numberOfRows = bottom - top; const pluginState = getPluginState(editorState); const options = [{ id: 'editor.table.insertColumn', title: formatMessage(messages.insertColumn), onClick: (state, dispatch, view) => { const selectionRect = getClosestSelectionRect(state); const index = selectionRect === null || selectionRect === void 0 ? void 0 : selectionRect.right; if (index) { insertColumnWithAnalytics(api, editorAnalyticsAPI, isTableScalingEnabled, isTableFixedColumnWidthsOptionEnabled, shouldUseIncreasedScalingPercent, isCommentEditor)(INPUT_METHOD.FLOATING_TB, index)(state, dispatch, view); } return true; }, selected: false, disabled: false, // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766 elemAfter: jsx("div", { css: shortcutStyle }, tooltip(addColumnAfter)) }, { id: 'editor.table.insertRow', title: formatMessage(messages.insertRow), onClick: (state, dispatch) => { const selectionRect = getClosestSelectionRect(state); const index = selectionRect === null || selectionRect === void 0 ? void 0 : selectionRect.bottom; if (index) { insertRowWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.FLOATING_TB, { index, moveCursorToInsertedRow: true })(state, dispatch); } return true; }, selected: false, disabled: false, // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766 elemAfter: jsx("div", { css: shortcutStyle }, tooltip(addRowAfter)) }, { id: 'editor.table.removeColumns', title: formatMessage(messages.removeColumns, { 0: numberOfColumns }), onClick: (state, dispatch, view) => { const selectionRect = getClosestSelectionRect(state); if (selectionRect) { deleteColumnsWithAnalytics(editorAnalyticsAPI, api, isTableScalingEnabled, isTableFixedColumnWidthsOptionEnabled, shouldUseIncreasedScalingPercent, isCommentEditor)(INPUT_METHOD.FLOATING_TB, selectionRect)(state, dispatch, view); } return true; }, onFocus: highlightColumnsHandler, onBlur: clearHoverSelection(), onMouseOver: highlightColumnsHandler, onMouseLeave: clearHoverSelection(), selected: false, disabled: false }, { id: 'editor.table.removeRows', title: formatMessage(messages.removeRows, { 0: numberOfRows }), onClick: (state, dispatch) => { const selectionRect = getClosestSelectionRect(state); if (selectionRect) { deleteRowsWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.FLOATING_TB, selectionRect, false)(state, dispatch); } return true; }, onFocus: highlightRowsHandler, onBlur: clearHoverSelection(), onMouseOver: highlightRowsHandler, onMouseLeave: clearHoverSelection(), selected: false, disabled: false }]; if (pluginState.pluginConfig.allowMergeCells) { options.push({ id: 'editor.table.mergeCells', title: formatMessage(messages.mergeCells), onClick: mergeCellsWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.FLOATING_TB), selected: false, disabled: !canMergeCells(editorState.tr) }, { id: 'editor.table.splitCell', title: formatMessage(messages.splitCell), onClick: splitCellWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.FLOATING_TB), selected: false, disabled: !splitCell(editorState) }); } if (pluginState !== null && pluginState !== void 0 && (_pluginState$pluginCo = pluginState.pluginConfig) !== null && _pluginState$pluginCo !== void 0 && _pluginState$pluginCo.allowDistributeColumns) { let wouldChange = true; // Default to enabled - show the button. let newResizeStateWithAnalytics; // Performance optimization: Skip expensive getTableScalingPercent() DOM query when limited mode is enabled. // This avoids layout reflows on every transaction. Instead, button stays enabled and calculates on-demand when clicked. if (!isLimitedModeEnabled && !expValEquals('platform_editor_table_toolbar_perf_fix', 'isEnabled', true)) { var _newResizeStateWithAn, _newResizeStateWithAn2; newResizeStateWithAnalytics = editorView ? getNewResizeStateFromSelectedColumns(initialSelectionRect, editorState, editorView.domAtPos.bind(editorView), getEditorContainerWidth, isTableScalingEnabled, isTableFixedColumnWidthsOptionEnabled, isCommentEditor) : undefined; wouldChange = (_newResizeStateWithAn = (_newResizeStateWithAn2 = newResizeStateWithAnalytics) === null || _newResizeStateWithAn2 === void 0 ? void 0 : _newResizeStateWithAn2.changed) !== null && _newResizeStateWithAn !== void 0 ? _newResizeStateWithAn : false; } const distributeColumnWidths = (state, dispatch, view) => { // When optimization is enabled, calculate on-demand when clicked if (isLimitedModeEnabled || expValEquals('platform_editor_table_toolbar_perf_fix', 'isEnabled', true)) { if (view) { const resizeState = getNewResizeStateFromSelectedColumns(initialSelectionRect, state, view.domAtPos.bind(view), getEditorContainerWidth, isTableScalingEnabled, isTableFixedColumnWidthsOptionEnabled, isCommentEditor); if (resizeState) { distributeColumnsWidthsWithAnalytics(editorAnalyticsAPI, api)(INPUT_METHOD.FLOATING_TB, resizeState)(state, dispatch); return true; } } return false; } else { // Original behavior: use pre-calculated state if (newResizeStateWithAnalytics) { distributeColumnsWidthsWithAnalytics(editorAnalyticsAPI, api)(INPUT_METHOD.FLOATING_TB, newResizeStateWithAnalytics)(state, dispatch); return true; } return false; } }; options.push({ id: 'editor.table.distributeColumns', title: formatMessage(messages.distributeColumns), onClick: distributeColumnWidths, selected: false, disabled: !wouldChange }); } if (pluginState !== null && pluginState !== void 0 && (_pluginState$pluginCo2 = pluginState.pluginConfig) !== null && _pluginState$pluginCo2 !== void 0 && _pluginState$pluginCo2.allowColumnSorting) { const hasMergedCellsInTable = getMergedCellsPositions(editorState.tr).length > 0; const warning = hasMergedCellsInTable ? formatMessage(messages.canNotSortTable) : undefined; options.push({ id: 'editor.table.sortColumnAsc', title: formatMessage(messages.sortColumnASC), onMouseOver: (state, dispatch) => { if (getMergedCellsPositions(state.tr).length !== 0) { hoverMergedCells()(state, dispatch); return true; } return false; }, onMouseOut: (state, dispatch) => { clearHoverSelection()(state, dispatch); return true; }, onClick: (state, dispatch) => { sortColumnWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.FLOATING_TB, initialSelectionRect.left, SortOrder.ASC)(state, dispatch); return true; }, selected: false, disabled: hasMergedCellsInTable, tooltip: warning }); options.push({ id: 'editor.table.sortColumnDesc', title: formatMessage(messages.sortColumnDESC), onMouseOver: (state, dispatch) => { if (getMergedCellsPositions(state.tr).length !== 0) { hoverMergedCells()(state, dispatch); return true; } return false; }, onMouseOut: (state, dispatch) => { clearHoverSelection()(state, dispatch); return true; }, onClick: (state, dispatch) => { sortColumnWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.FLOATING_TB, initialSelectionRect.left, SortOrder.DESC)(state, dispatch); return true; }, selected: false, disabled: hasMergedCellsInTable, tooltip: warning }); } options.push({ id: 'editor.table.clearCells', title: formatMessage(messages.clearCells, { 0: Math.max(numberOfColumns, numberOfRows) }), onClick: (state, dispatch) => { const { targetCellPosition } = getPluginState(state); emptyMultipleCellsWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.FLOATING_TB, targetCellPosition)(state, dispatch); return true; }, selected: false, disabled: false, // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766 elemAfter: jsx("div", { css: shortcutStyle }, tooltip(backspace)) }); return { id: 'editor.table.cellOptions', testId: 'cell_options', type: 'dropdown', title: formatMessage(messages.cellOptions), options, // Increased dropdown item width to prevent labels from being truncated dropdownWidth: 230, showSelected: false }; }; export const getClosestSelectionRect = state => { const selection = state.selection; return isSelectionType(selection, 'cell') ? // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion getSelectionRect(selection) : findCellRectClosestToPos(selection.$from); }; const getClosestSelectionOrTableRect = state => { const selection = state.selection; const tableObject = findTable(state.selection); if (!tableObject) { return; } const map = TableMap.get(tableObject.node); const tableRect = new Rect(0, 0, map.width, map.height); // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return isSelectionType(selection, 'cell') ? getSelectionRect(selection) : tableRect; }; // Memoize the expensive DOM queries (querySelector, closestElement) separately // Cache key is the parent DOM node reference - stays same when typing in same cell const getTableWrapperFromParentImpl = parent => { if (!parent) { return undefined; } // These are the expensive DOM operations const tableRef = // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting parent.querySelector('table') || undefined; if (!tableRef) { return undefined; } return closestElement(tableRef, `.${TableCssClassName.TABLE_NODE_WRAPPER}`) || undefined; }; // Create memoized version ONCE - reused across all calls const getMemoizedTableWrapperFromParent = memoizeOne(getTableWrapperFromParentImpl); export const getToolbarConfig = (getEditorContainerWidth, api, editorAnalyticsAPI, getEditorView, options, isTableFixedColumnWidthsOptionEnabled = false, shouldUseIncreasedScalingPercent = false) => config => (state, intl) => { const tableObject = findTable(state.selection); const pluginState = getPluginState(state); const resizeState = tableResizingPluginKey.getState(state); const tableWidthState = tableWidthPluginKey.getState(state); const isTableScalingEnabled = (options === null || options === void 0 ? void 0 : options.isTableScalingEnabled) || false; const nodeType = state.schema.nodes.table; const toolbarTitle = 'Table floating controls'; const areAnyNewToolbarFlagsEnabled = areToolbarFlagsEnabled(Boolean(api === null || api === void 0 ? void 0 : api.toolbar)); if (editorExperiment('platform_editor_controls', 'variant1')) { var _api$editorViewMode, _api$editorViewMode$s; let isDragHandleMenuOpened = false; let isTableRowOrColumnDragged = false; if (options !== null && options !== void 0 && options.dragAndDropEnabled) { const { isDragMenuOpen = false, isDragging = false } = getDragDropPluginState(state); isDragHandleMenuOpened = isDragMenuOpen; isTableRowOrColumnDragged = isDragging; } const isTableOrColumnResizing = !!(resizeState !== null && resizeState !== void 0 && resizeState.dragging || tableWidthState !== null && tableWidthState !== void 0 && tableWidthState.resizing); const isTableMenuOpened = pluginState.isContextualMenuOpen || isDragHandleMenuOpened; const isTableState = isTableRowOrColumnDragged || isTableOrColumnResizing || isTableMenuOpened; const isViewMode = (api === null || api === void 0 ? void 0 : (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : (_api$editorViewMode$s = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode$s === void 0 ? void 0 : _api$editorViewMode$s.mode) === 'view'; // Note: when focus is in codeblocks, pluginState.editorHasFocus is false, so the codeblocks toolbar // won't be suppressed. const shouldSuppressAllToolbars = isTableState && pluginState.editorHasFocus && !isViewMode; if (shouldSuppressAllToolbars) { return { title: toolbarTitle, items: [], nodeType }; } } // We don't want to show floating toolbar while resizing the table const isWidthResizing = tableWidthState === null || tableWidthState === void 0 ? void 0 : tableWidthState.resizing; if (tableObject && pluginState.editorHasFocus && !isWidthResizing) { var _api$limitedMode$shar, _api$limitedMode, _api$limitedMode$shar2, _api$extension, _api$extension$shared, _api$extension2; const isNested = pluginState.tablePos && isTableNested(state, pluginState.tablePos); const isTableScalingWithFixedColumnWidthsOptionShown = isTableScalingEnabled && isTableFixedColumnWidthsOptionEnabled && !isNested; const areTableColumWidthsFixed = tableObject.node.attrs.displayMode === 'fixed'; const editorView = getEditorView(); const getDomRef = expValEquals('platform_editor_table_toolbar_perf_fix', 'isEnabled', true) ? editorView => { const domAtPos = editorView.domAtPos.bind(editorView); const parent = findParentDomRefOfType(nodeType, domAtPos)(state.selection); return getMemoizedTableWrapperFromParent(parent); } : editorView => { let element; const domAtPos = editorView.domAtPos.bind(editorView); const parent = findParentDomRefOfType(nodeType, domAtPos)(state.selection); if (parent) { const tableRef = // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting parent.querySelector('table') || undefined; if (tableRef) { element = closestElement(tableRef, `.${TableCssClassName.TABLE_NODE_WRAPPER}`) || undefined; } } return element; }; const menu = getToolbarMenuConfig(config, pluginState, intl, editorAnalyticsAPI, isTableScalingWithFixedColumnWidthsOptionShown, areTableColumWidthsFixed); const alignmentMenu = config.allowTableAlignment && !isNested ? getAlignmentOptionsConfig(state, intl, editorAnalyticsAPI, getEditorContainerWidth, editorView, shouldUseIncreasedScalingPercent, areAnyNewToolbarFlagsEnabled, options === null || options === void 0 ? void 0 : options.fullWidthEnabled, options === null || options === void 0 ? void 0 : options.isCommentEditor) : []; const isLimitedModeEnabled = (_api$limitedMode$shar = api === null || api === void 0 ? void 0 : (_api$limitedMode = api.limitedMode) === null || _api$limitedMode === void 0 ? void 0 : (_api$limitedMode$shar2 = _api$limitedMode.sharedState.currentState()) === null || _api$limitedMode$shar2 === void 0 ? void 0 : _api$limitedMode$shar2.enabled) !== null && _api$limitedMode$shar !== void 0 ? _api$limitedMode$shar : false; const cellItems = pluginState.isDragAndDropEnabled ? [] : getCellItems(state, editorView, intl, getEditorContainerWidth, api, editorAnalyticsAPI, isTableScalingEnabled, isTableFixedColumnWidthsOptionEnabled, shouldUseIncreasedScalingPercent, options === null || options === void 0 ? void 0 : options.isCommentEditor, isLimitedModeEnabled); const columnSettingsItems = pluginState.isDragAndDropEnabled ? getColumnSettingItems(state, editorView, intl, getEditorContainerWidth, api, editorAnalyticsAPI, isTableScalingEnabled, isTableFixedColumnWidthsOptionEnabled, options === null || options === void 0 ? void 0 : options.isCommentEditor, isLimitedModeEnabled) : []; const colorPicker = !areAnyNewToolbarFlagsEnabled ? getColorPicker(state, menu, intl, editorAnalyticsAPI, getEditorView) : []; // Check if we need to show confirm dialog for delete button let confirmDialog; if (isReferencedSource(state, tableObject.node)) { const localSourceName = intl.formatMessage(messages.unnamedSource); confirmDialog = () => ({ title: intl.formatMessage(messages.deleteElementTitle), okButtonLabel: intl.formatMessage(messages.confirmDeleteLinkedModalOKButton), message: intl.formatMessage(messages.confirmDeleteLinkedModalMessage, { nodeName: getNodeName(state, tableObject.node) || localSourceName }), messagePrefix: intl.formatMessage(messages.confirmDeleteLinkedModalMessagePrefix), isReferentialityDialog: true, getChildrenInfo: () => getChildrenInfo(state, tableObject.node), checkboxLabel: intl.formatMessage(messages.confirmModalCheckboxLabel), onConfirm: (isChecked = false) => clickWithCheckboxHandler(isChecked, tableObject.node) }); } const deleteButton = { id: 'editor.table.delete', type: 'button', appearance: 'danger', icon: DeleteIcon, onClick: deleteTableWithAnalytics(editorAnalyticsAPI), disabled: !!resizeState && !!resizeState.dragging, onMouseEnter: hoverTable(true), onFocus: hoverTable(true), onBlur: clearHoverSelection(), onMouseLeave: clearHoverSelection(), title: intl.formatMessage(commonMessages.remove), focusEditoronEnter: true, confirmDialog }; const copyButton = { type: 'copy-button', supportsViewMode: true, items: [{ state, formatMessage: intl.formatMessage, nodeType, onMouseEnter: hoverTable(false, true), onMouseLeave: clearHoverSelection(), onFocus: hoverTable(false, true), onBlur: clearHoverSelection() }] }; const isNestedTable = isNestedTablesSupported(state.schema) && isSelectionTableNestedInTable(state); const hoverTableProps = (isInDanger, isSelected) => ({ onMouseEnter: hoverTable(isInDanger, isSelected), onMouseLeave: clearHoverSelection(), onFocus: hoverTable(isInDanger, isSelected), onBlur: clearHoverSelection() }); // testId is required to show focus on trigger button on ESC key press // see hideOnEsc in platform/packages/editor/editor-plugin-floating-toolbar/src/ui/Dropdown.tsx const overflowDropdownTestId = 'table-overflow-dropdown-trigger'; const extensionState = api === null || api === void 0 ? void 0 : (_api$extension = api.extension) === null || _api$extension === void 0 ? void 0 : (_api$extension$shared = _api$extension.sharedState) === null || _api$extension$shared === void 0 ? void 0 : _api$extension$shared.currentState(); const extensionApi = api === null || api === void 0 ? void 0 : (_api$extension2 = api.extension) === null || _api$extension2 === void 0 ? void 0 : _api$extension2.actions.api(); return { title: toolbarTitle, getDomRef, nodeType, offset: [0, 18], absoluteOffset: { top: -6 }, zIndex: akEditorFloatingPanelZIndex + 1, // Place the context menu slightly above the others items: [menu, ...(!areAnyNewToolbarFlagsEnabled ? [separator(menu.hidden)] : []), ...alignmentMenu, ...(!areAnyNewToolbarFlagsEnabled ? [separator(alignmentMenu.length === 0)] : []), ...cellItems, ...columnSettingsItems, ...colorPicker, ...(!areAnyNewToolbarFlagsEnabled ? [{ type: 'extensions-placeholder', separator: 'end' }, copyButton, { type: 'separator' }, deleteButton] : [areAnyNewToolbarFlagsEnabled && { type: 'separator', fullHeight: true }, { type: 'overflow-dropdown', testId: overflowDropdownTestId, dropdownWidth: 220, options: [{ type: 'custom', fallback: [], render: (editorView, dropdownOptions) => { if (!editorView) { return null; } if (!extensionApi || !(extensionState !== null && extensionState !== void 0 && extensionState.extensionProvider)) { return null; } return jsx(DropdownMenuExtensionItems, { node: tableObject.node, editorView: editorView // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , extension: { extensionProvider: extensionState !== null && extensionState !== void 0 && extensionState.extensionProvider ? Promise.resolve(extensionState.extensionProvider) : undefined, extensionApi: extensionApi }, dropdownOptions: dropdownOptions // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , disabled: key => { return isNestedTable && ['referentiality:connections', 'chart:insert-chart'].includes(key); }, areAnyNewToolbarFlagsEnabled: areAnyNewToolbarFlagsEnabled }); } }, ...(extensionApi && extensionState !== null && extensionState !== void 0 && extensionState.extensionProvider && !areAnyNewToolbarFlagsEnabled ? [{ type: 'separator' }] : []), { title: intl.formatMessage(commonMessages.copyToClipboard), onClick: () => { var _api$core, _api$floatingToolbar; api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute( // @ts-ignore api === null || api === void 0 ? void 0 : (_api$floatingToolbar = api.floatingToolbar) === null || _api$floatingToolbar === void 0 ? void 0 : _api$floatingToolbar.commands.copyNode(nodeType, INPUT_METHOD.FLOATING_TB)); return true; }, icon: jsx(CopyIcon, { label: intl.formatMessage(commonMessages.copyToClipboard) }), ...hoverTableProps(false, true) }, { title: intl.formatMessage(commonMessages.delete), onClick: deleteTableWithAnalytics(editorAnalyticsAPI), icon: jsx(DeleteIcon, { label: intl.formatMessage(commonMessages.delete) }), ...hoverTableProps(true), confirmDialog }] }])], scrollable: true }; } return; }; const separator = hidden => { return { type: 'separator', hidden: hidden }; }; const getCellItems = (state, view, { formatMessage }, getEditorContainerWidth, api, editorAnalyticsAPI, isTableScalingEnabled = false, isTableFixedColumnWidthsOptionEnabled = false, shouldUseIncreasedScalingPercent = false, isCommentEditor = false, isLimitedModeEnabled = false) => { const initialSelectionRect = getClosestSelectionRect(state); if (initialSelectionRect) { const cellOptions = getToolbarCellOptionsConfig(state, view, initialSelectionRect, { formatMessage }, getEditorContainerWidth, api, editorAnalyticsAPI, isTableScalingEnabled, isTableFixedColumnWidthsOptionEnabled, shouldUseIncreasedScalingPercent, isCommentEditor, isLimitedModeEnabled); // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return [cellOptions, separator(cellOptions.hidden)]; } return []; }; const getDistributeConfig = (getEditorContainerWidth, api, editorAnalyticsAPI, isTableScalingEnabled = false, isTableFixedColumnWidthsOptionEnabled = false, isCommentEditor = false) => (state, dispatch, editorView) => { const selectionOrTableRect = getClosestSelectionOrTableRect(state); if (!editorView || !selectionOrTableRect) { return false; } const newResizeStateWithAnalytics = getNewResizeStateFromSelectedColumns(selectionOrTableRect, state, editorView.domAtPos.bind(editorView), getEditorContainerWidth, isTableScalingEnabled, isTableFixedColumnWidthsOptionEnabled, isCommentEditor); if (newResizeStateWithAnalytics) { distributeColumnsWidthsWithAnalytics(editorAnalyticsAPI, api)(INPUT_METHOD.FLOATING_TB, newResizeStateWithAnalytics)(state, dispatch); return true; } return false; }; // this create the button group for distribute column and also fixed column width // fixed column button should be in this function call in the future const getColumnSettingItems = (editorState, editorView, { formatMessage }, getEditorContainerWidth, api, editorAnalyticsAPI, isTableScalingEnabled = false, isTableFixedColumnWidthsOptionEnabled = false, isCommentEditor = false, isLimitedModeEnabled = false) => { var _pluginState$pluginCo3; const pluginState = getPluginState(editorState); const items = []; let wouldChange = true; // Default to enabled - show the button. let newResizeStateWithAnalytics; if (expValEquals('platform_editor_table_toolbar_perf_fix', 'isEnabled', true)) { if (!editorView) { return []; } } else { const selectionOrTableRect = getClosestSelectionOrTableRect(editorState); if (!selectionOrTableRect || !editorView) { return []; } // Performance optimization: Skip expensive getTableScalingPercent() DOM query when limited mode is enabled. // This avoids layout reflows on every transaction. Instead, button stays enabled and calculates on-demand when clicked. if (!isLimitedModeEnabled) { var _newResizeStateWithAn3, _newResizeStateWithAn4; newResizeStateWithAnalytics = getNewResizeStateFromSelectedColumns(selectionOrTableRect, editorState, editorView.domAtPos.bind(editorView), getEditorContainerWidth, isTableScalingEnabled, isTableFixedColumnWidthsOptionEnabled, isCommentEditor); wouldChange = (_newResizeStateWithAn3 = (_newResizeStateWithAn4 = newResizeStateWithAnalytics) === null || _newResizeStateWithAn4 === void 0 ? void 0 : _newResizeStateWithAn4.changed) !== null && _newResizeStateWithAn3 !== void 0 ? _newResizeStateWithAn3 : false; } } if (pluginState !== null && pluginState !== void 0 && (_pluginState$pluginCo3 = pluginState.pluginConfig) !== null && _pluginState$pluginCo3 !== void 0 && _pluginState$pluginCo3.allowDistributeColumns && pluginState.isDragAndDropEnabled) { items.push({ id: 'editor.table.distributeColumns', type: 'button', title: formatMessage(messages.distributeColumns), icon: () => jsx(TableColumnsDistributeIcon, { spacing: 'spacious', label: '' }), onClick: (state, dispatch, view) => getDistributeConfig(getEditorContainerWidth, api, editorAnalyticsAPI, isTableScalingEnabled, isTableFixedColumnWidthsOptionEnabled, isCommentEditor)(state, dispatch, view), disabled: !wouldChange }); } if (items.length !== 0 && !areToolbarFlagsEnabled(Boolean(api === null || api === void 0 ? void 0 : api.toolbar))) { items.push({ type: 'separator' }); } return items; }; const getColorPicker = (state, menu, { formatMessage }, editorAnalyticsAPI, getEditorView) => { var _node$attrs; const { targetCellPosition, pluginConfig } = getPluginState(state); if (!pluginConfig.allowBackgroundColor) { return []; } const node = targetCellPosition ? state.doc.nodeAt(targetCellPosition) : undefined; const currentBackground = (node === null || node === void 0 ? void 0 : (_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.background) || '#ffffff'; const defaultPalette = cellBackgroundColorPalette.find(item => item.value === currentBackground) || { // eslint-disable-next-line @atlassian/i18n/no-literal-string-in-object label: 'Custom', value: currentBackground, border: DEFAULT_BORDER_COLOR }; return [{ id: 'editor.table.colorPicker', title: formatMessage(messages.cellBackground), type: 'select', isAriaExpanded: true, selectType: 'color', defaultValue: defaultPalette, options: cellBackgroundColorPalette, returnEscToButton: true, // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any onChange: option => setColorWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.FLOATING_TB, option.value, getEditorView()) }, separator(menu.hidden)]; }; const clickWithCheckboxHandler = (isChecked, node, editorAnalyticsAPI) => (state, dispatch) => { if (!node) { return false; } if (!isChecked) { return deleteTableWithAnalytics(editorAnalyticsAPI)(state, dispatch); } else { removeDescendantNodes(node)(state, dispatch); } return true; }; const highlightRowsHandler = (state, dispatch) => { const selectionRect = getClosestSelectionRect(state); if (selectionRect) { hoverRows(getSelectedRowIndexes(selectionRect), true)(state, dispatch); return true; } return false; }; const highlightColumnsHandler = (state, dispatch) => { const selectionRect = getClosestSelectionRect(state); if (selectionRect) { hoverColumns(getSelectedColumnIndexes(selectionRect), true)(state, dispatch); return true; } return false; }; const getAlignmentOptionsConfig = (editorState, { formatMessage }, editorAnalyticsAPI, getEditorContainerWidth, editorView, shouldUseIncreasedScalingPercent, areAnyNewToolbarFlagsEnabled, isFullWidthEditor, isCommentEditor) => { const tableObject = findTable(editorState.selection); if (!tableObject) { return []; } const alignmentIcons = [{ id: 'editor.table.alignLeft', value: 'align-start', icon: () => jsx(AlignImageLeftIcon, { color: "currentColor", spacing: "spacious", label: "table-align-start-icon" }) }, { id: 'editor.table.alignCenter', value: 'center', icon: () => jsx(AlignImageCenterIcon, { color: "currentColor", spacing: "spacious", label: "table-align-center-icon" }) }]; // eslint-disable-next-line @typescript-eslint/no-explicit-any const layoutToMessages = { center: messages.alignTableCenter, 'align-start': messages.alignTableLeft }; const alignmentButtons = alignmentIcons.map(alignmentIcon => { const { id, value, icon } = alignmentIcon; const currentLayout = tableObject.node.attrs.layout; const shouldDisableLayoutOption = expValEquals('platform_editor_table_toolbar_perf_fix', 'isEnabled', true) ? getMemoizedIsLayoutOptionDisabled(tableObject.node, getEditorContainerWidth, editorView !== null, shouldUseIncreasedScalingPercent, isFullWidthEditor) : isLayoutOptionDisabled(tableObject.node, getEditorContainerWidth, editorView, shouldUseIncreasedScalingPercent, isFullWidthEditor); return { id: id, type: 'button', icon: icon, title: formatMessage(layoutToMessages[value]), selected: normaliseAlignment(currentLayout) === value, onClick: setTableAlignmentWithAnalytics(editorAnalyticsAPI, isCommentEditor || false)(value, currentLayout, INPUT_METHOD.FLOATING_TB, CHANGE_ALIGNMENT_REASON.TOOLBAR_OPTION_CHANGED), ...(shouldDisableLayoutOption && { disabled: value !== 'center' }) }; }); const alignmentItemOptions = { render: props => { return jsx(FloatingAlignmentButtons, _extends({ alignmentButtons: alignmentButtons // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading }, props, { areAnyNewToolbarFlagsEnabled: areAnyNewToolbarFlagsEnabled })); }, width: 74, height: 32 }; const selectedAlignmentIcon = getSelectedAlignmentIcon(alignmentIcons, tableObject.node); const alignmentToolbarItem = [{ id: 'table-layout', testId: 'table-layout-dropdown', type: 'dropdown', options: alignmentItemOptions, title: formatMessage(messages.tableAlignmentOptions), icon: selectedAlignmentIcon === null || selectedAlignmentIcon === void 0 ? void 0 : selectedAlignmentIcon.icon }]; return alignmentToolbarItem; }; const getSelectedAlignmentIcon = (alignmentIcons, selectedNode) => { const selectedAlignment = selectedNode.attrs.layout; return alignmentIcons.find(icon => icon.value === normaliseAlignment(selectedAlignment)); }; const isLayoutOptionDisabledImpl = (selectedNode, getEditorContainerWidth, hasEditorView, shouldUseIncreasedScalingPercent, isFullWidthEditor) => { const { lineLength } = getEditorContainerWidth(); let tableContainerWidth = getTableContainerWidth(selectedNode); // table may be scaled, use the scale percent to calculate the table width if (hasEditorView) { const tableWrapperWidth = tableContainerWidth; const scalePercent = getStaticTableScalingPercent(selectedNode, tableWrapperWidth, shouldUseIncreasedScalingPercent); tableContainerWidth = tableContainerWidth * scalePercent; } // If fixed-width editor, we disable 'left-alignment' when table width is 760px. // tableContainerWidth +1 here because tableContainerWidth is 759 in fixed-width editor if (selectedNode && !isFullWidthEditor && lineLength && tableContainerWidth + 1 >= lineLength) { return true; } return false; }; const getMemoizedIsLayoutOptionDisabled = memoizeOne(isLayoutOptionDisabledImpl, ([prevNode, ...prevRest], [nextNode, ...nextRest]) => { // Only node needs special comparison (attrs only), rest use reference equality const nodeEqual = isEqual(prevNode.attrs, nextNode.attrs); const restEqual = prevRest.every((val, idx) => val === nextRest[idx]); return nodeEqual && restEqual; }); const isLayoutOptionDisabled = (selectedNode, getEditorContainerWidth, editorView, shouldUseIncreasedScalingPercent, isFullWidthEditor) => { const { lineLength } = getEditorContainerWidth(); let tableContainerWidth = getTableContainerWidth(selectedNode); // table may be scaled, use the scale percent to calculate the table width if (editorView) { const tableWrapperWidth = tableContainerWidth; const scalePercent = getStaticTableScalingPercent(selectedNode, tableWrapperWidth, shouldUseIncreasedScalingPercent); tableContainerWidth = tableContainerWidth * scalePercent; } // If fixed-width editor, we disable 'left-alignment' when table width is 760px. // tableContainerWidth +1 here because tableContainerWidth is 759 in fixed-width editor if (selectedNode && !isFullWidthEditor && lineLength && tableContainerWidth + 1 >= lineLength) { return true; } return false; };