UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

588 lines (581 loc) 23.1 kB
/* eslint-disable @atlaskit/design-system/prefer-primitives */ /** * @jsxRuntime classic * @jsx jsx */ /** @jsxFrag */ import React, { useEffect, useState } from 'react'; // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766 import { jsx } from '@emotion/react'; import { injectIntl } from 'react-intl-next'; import { INPUT_METHOD } from '@atlaskit/editor-common/analytics'; import { tableMessages as messages } from '@atlaskit/editor-common/messages'; import { DropdownMenuSharedCssClassName } from '@atlaskit/editor-common/styles'; import { backgroundPaletteTooltipMessages, cellBackgroundColorPalette, ColorPalette, getSelectedRowAndColumnFromPalette } from '@atlaskit/editor-common/ui-color'; import { ArrowKeyNavigationProvider, ArrowKeyNavigationType } from '@atlaskit/editor-common/ui-menu'; import { UserIntentPopupWrapper } from '@atlaskit/editor-common/user-intent'; import { closestElement } from '@atlaskit/editor-common/utils'; import { hexToEditorBackgroundPaletteColor } from '@atlaskit/editor-palette'; import { shortcutStyle } from '@atlaskit/editor-shared-styles/shortcut'; import { TableMap } from '@atlaskit/editor-tables/table-map'; import { findCellRectClosestToPos, getSelectionRect, isSelectionType } from '@atlaskit/editor-tables/utils'; import PaintBucketIcon from '@atlaskit/icon/core/paint-bucket'; import { fg } from '@atlaskit/platform-feature-flags'; // eslint-disable-next-line @atlaskit/design-system/no-emotion-primitives -- to be migrated to @atlaskit/primitives/compiled – go/akcss import { Box, xcss } from '@atlaskit/primitives'; import Toggle from '@atlaskit/toggle'; import { clearHoverSelection, hoverColumns, hoverRows } from '../../pm-plugins/commands'; import { setColorWithAnalytics, toggleHeaderColumnWithAnalytics, toggleHeaderRowWithAnalytics, toggleNumberColumnWithAnalytics } from '../../pm-plugins/commands/commands-with-analytics'; import { toggleDragMenu } from '../../pm-plugins/drag-and-drop/commands'; import { getPluginState } from '../../pm-plugins/drag-and-drop/plugin-factory'; import { getPluginState as getTablePluginState } from '../../pm-plugins/plugin-factory'; import { getDragMenuConfig } from '../../pm-plugins/utils/drag-menu'; import { checkIfHeaderColumnEnabled, checkIfHeaderRowEnabled, checkIfNumberColumnEnabled } from '../../pm-plugins/utils/nodes'; import { getSelectedColumnIndexes, getSelectedRowIndexes } from '../../pm-plugins/utils/selection'; import { TableCssClassName as ClassName } from '../../types'; import { colorPalletteColumns } from '../consts'; import { DropdownMenu } from './DropdownMenu'; import { cellColourPreviewStyles, dragMenuBackgroundColorStyles, toggleStyles } from './styles'; const MapDragMenuOptionIdToMessage = { add_row_above: { message: messages.addRowAbove, plural: null }, add_row_below: { message: messages.addRowBelow, plural: null }, add_column_left: { message: messages.addColumnLeft, plural: null }, add_column_right: { message: messages.addColumnRight, plural: null }, distribute_columns: { message: messages.distributeColumns, plural: 'noOfCols' }, clear_cells: { message: messages.clearCells, plural: 'noOfCells' }, delete_row: { message: messages.removeRows, plural: 'noOfRows' }, delete_column: { message: messages.removeColumns, plural: 'noOfCols' }, move_column_left: { message: messages.moveColumnLeft, plural: 'noOfCols' }, move_column_right: { message: messages.moveColumnRight, plural: 'noOfCols' }, move_row_up: { message: messages.moveRowUp, plural: 'noOfRows' }, move_row_down: { message: messages.moveRowDown, plural: 'noOfRows' }, sort_column_asc: { message: messages.sortColumnIncreasing, plural: null }, sort_column_desc: { message: messages.sortColumnDecreasing, plural: null } }; const getGroupedDragMenuConfig = () => { const groupedDragMenuConfig = [['add_row_above', 'add_row_below', 'add_column_left', 'add_column_right', 'distribute_columns', 'clear_cells', 'delete_row', 'delete_column'], ['move_column_left', 'move_column_right', 'move_row_up', 'move_row_down']]; const sortColumnItems = ['sort_column_asc', 'sort_column_desc']; groupedDragMenuConfig.unshift(sortColumnItems); return groupedDragMenuConfig; }; const elementBeforeIconStyles = xcss({ marginRight: 'space.negative.075', display: 'flex' }); const convertToDropdownItems = (dragMenuConfig, formatMessage, selectionRect) => { const groupedDragMenuConfig = getGroupedDragMenuConfig(); const menuItemsArr = [...Array(groupedDragMenuConfig.length)].map(() => []); const menuCallback = {}; dragMenuConfig.forEach(item => { var _MapDragMenuOptionIdT; const menuGroupIndex = groupedDragMenuConfig.findIndex(group => group.includes(item.id)); if (menuGroupIndex === -1) { return; } const isPlural = Boolean((_MapDragMenuOptionIdT = MapDragMenuOptionIdToMessage[item.id]) === null || _MapDragMenuOptionIdT === void 0 ? void 0 : _MapDragMenuOptionIdT.plural); let plural = 0; if (isPlural && selectionRect) { const { top, bottom, right, left } = selectionRect; switch (MapDragMenuOptionIdToMessage[item.id].plural) { case 'noOfCols': { plural = right - left; break; } case 'noOfRows': { plural = bottom - top; break; } case 'noOfCells': { plural = Math.max(right - left, bottom - top); break; } } } const options = isPlural ? { 0: plural } : undefined; menuItemsArr[menuGroupIndex].push({ key: item.id, content: formatMessage(MapDragMenuOptionIdToMessage[item.id].message, options), value: { name: item.id }, isDisabled: item.disabled, elemBefore: item.icon ? jsx(Box, { xcss: elementBeforeIconStyles }, jsx(item.icon, { color: "currentColor", spacing: "spacious", label: formatMessage(MapDragMenuOptionIdToMessage[item.id].message, options) })) : undefined, // 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: item.keymap ? jsx("div", { css: shortcutStyle }, item.keymap) : undefined }); item.onClick && (menuCallback[item.id] = item.onClick); }); const menuItems = menuItemsArr.reduce((acc, curr) => { (curr === null || curr === void 0 ? void 0 : curr.length) > 0 && acc.push({ items: curr }); return acc; }, []); return { menuItems, menuCallback }; }; const DragMenu = /*#__PURE__*/React.memo(({ direction = 'row', index, target, isOpen, editorView, tableNode, targetCellPosition, getEditorContainerWidth, api, editorAnalyticsAPI, pluginConfig, intl: { formatMessage }, fitHeight, fitWidth, mountPoint, scrollableElement, boundariesElement, isTableScalingEnabled, shouldUseIncreasedScalingPercent, isTableFixedColumnWidthsOptionEnabled, ariaNotifyPlugin, isCommentEditor }) => { var _tableMap$hasMergedCe, _pluginConfig$allowBa, _pluginConfig$allowCo; const { state, dispatch } = editorView; const { selection } = state; const tableMap = tableNode ? TableMap.get(tableNode) : undefined; const [isSubmenuOpen, setIsSubmenuOpen] = useState(false); const { isKeyboardModeActive } = getPluginState(state); const selectionRect = isSelectionType(selection, 'cell') ? // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion getSelectionRect(selection) : findCellRectClosestToPos(selection.$from); const hasMergedCellsInTable = (_tableMap$hasMergedCe = tableMap === null || tableMap === void 0 ? void 0 : tableMap.hasMergedCells()) !== null && _tableMap$hasMergedCe !== void 0 ? _tableMap$hasMergedCe : false; const allowBackgroundColor = (_pluginConfig$allowBa = pluginConfig === null || pluginConfig === void 0 ? void 0 : pluginConfig.allowBackgroundColor) !== null && _pluginConfig$allowBa !== void 0 ? _pluginConfig$allowBa : false; const dragMenuConfig = getDragMenuConfig(direction, getEditorContainerWidth, hasMergedCellsInTable, editorView, api, tableMap, index, targetCellPosition, selectionRect, editorAnalyticsAPI, pluginConfig === null || pluginConfig === void 0 ? void 0 : pluginConfig.isHeaderRowRequired, isTableScalingEnabled, isTableFixedColumnWidthsOptionEnabled, shouldUseIncreasedScalingPercent, ariaNotifyPlugin, isCommentEditor, (_pluginConfig$allowCo = pluginConfig === null || pluginConfig === void 0 ? void 0 : pluginConfig.allowColumnSorting) !== null && _pluginConfig$allowCo !== void 0 ? _pluginConfig$allowCo : true); const { menuItems, menuCallback } = convertToDropdownItems(dragMenuConfig, formatMessage, selectionRect); const handleSubMenuRef = ref => { // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting const dom = editorView.dom; const parent = closestElement(dom, '.fabric-editor-popup-scroll-parent') || closestElement(dom, '.ak-editor-content-area'); if (!(parent && ref)) { return; } const boundariesRect = parent.getBoundingClientRect(); const rect = ref.getBoundingClientRect(); if (!!mountPoint) { return; } const offsetParent = ref === null || ref === void 0 ? void 0 : ref.offsetParent; if (!offsetParent) { return; } const offsetParentRect = offsetParent.getBoundingClientRect(); const rightOverflow = offsetParentRect.right + rect.width - boundariesRect.right; const leftOverflow = boundariesRect.left - (offsetParentRect.left - rect.width); if (rightOverflow > leftOverflow) { ref.style.left = `-${rect.width}px`; } // if it overflows regardless of side, let it overlap with the parent menu if (leftOverflow > 0 && rightOverflow > 0) { if (rightOverflow < leftOverflow) { ref.style.left = `${offsetParentRect.width - rightOverflow}px`; } else { ref.style.left = `-${rect.width - leftOverflow}px`; } } }; const setColor = color => { const { state, dispatch } = editorView; setColorWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.CONTEXT_MENU, color)(state, dispatch); closeMenu(); setIsSubmenuOpen(false); }; const createBackgroundColorMenuItem = () => { var _node$attrs; const { targetCellPosition } = getTablePluginState(editorView.state); const node = targetCellPosition ? state.doc.nodeAt(targetCellPosition) : null; const background = hexToEditorBackgroundPaletteColor((node === null || node === void 0 ? void 0 : (_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.background) || '#ffffff'); const { selectedRowIndex, selectedColumnIndex } = getSelectedRowAndColumnFromPalette(cellBackgroundColorPalette, background, colorPalletteColumns); return { key: 'background', content: formatMessage(messages.backgroundColor), value: { name: 'background' }, elemBefore: jsx(Box, { xcss: elementBeforeIconStyles }, jsx(PaintBucketIcon, { color: "currentColor", spacing: "spacious", label: formatMessage(messages.backgroundColor) })), elemAfter: jsx("div", { // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 className: DropdownMenuSharedCssClassName.SUBMENU // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766 , css: dragMenuBackgroundColorStyles() }, jsx("div", { // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766 css: cellColourPreviewStyles(background) // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 , className: ClassName.DRAG_SUBMENU_ICON }), isSubmenuOpen && // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 jsx("div", { className: ClassName.DRAG_SUBMENU, ref: handleSubMenuRef }, jsx(ArrowKeyNavigationProvider, { type: ArrowKeyNavigationType.COLOR, selectedRowIndex: selectedRowIndex, selectedColumnIndex: selectedColumnIndex // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , handleClose: () => { const keyboardEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }); setIsSubmenuOpen(false); // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting target === null || target === void 0 ? void 0 : target.focus(); target === null || target === void 0 ? void 0 : target.dispatchEvent(keyboardEvent); }, isPopupPositioned: true, isOpenedByKeyboard: isKeyboardModeActive }, jsx(ColorPalette, { cols: colorPalletteColumns // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , onClick: color => { setColor(color); }, selectedColor: background // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , paletteOptions: { palette: cellBackgroundColorPalette, paletteColorTooltipMessages: backgroundPaletteTooltipMessages, hexToPaletteColor: hexToEditorBackgroundPaletteColor } })))) }; }; const toggleHeaderColumn = () => { toggleHeaderColumnWithAnalytics(editorAnalyticsAPI)(state, dispatch); }; const toggleHeaderRow = () => { toggleHeaderRowWithAnalytics(editorAnalyticsAPI)(state, dispatch); }; const toggleRowNumbers = () => { toggleNumberColumnWithAnalytics(editorAnalyticsAPI)(state, dispatch); }; const HeaderColumnToggle = () => // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766 jsx("div", { css: toggleStyles }, jsx(Toggle, { id: "toggle-header-column", label: formatMessage(messages.headerColumn), onChange: toggleHeaderColumn, isChecked: checkIfHeaderColumnEnabled(selection) })); const HeaderRowToggle = () => // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766 jsx("div", { css: toggleStyles }, jsx(Toggle, { id: "toggle-header-row", label: formatMessage(messages.headerRow), onChange: toggleHeaderRow, isChecked: checkIfHeaderRowEnabled(selection) })); const createHeaderRowColumnMenuItemOld = direction => { return direction === 'column' ? { key: 'header_column', content: formatMessage(messages.headerColumn), value: { name: 'header_column' }, elemAfter: jsx(HeaderColumnToggle, null) } : { key: 'header_row', content: formatMessage(messages.headerRow), value: { name: 'header_row' }, elemAfter: jsx(HeaderRowToggle, null) }; }; const createHeaderRowColumnMenuItem = direction => { if (direction === 'column' && (pluginConfig !== null && pluginConfig !== void 0 && pluginConfig.advanced || pluginConfig !== null && pluginConfig !== void 0 && pluginConfig.allowHeaderColumn)) { return { key: 'header_column', content: formatMessage(messages.headerColumn), value: { name: 'header_column' }, elemAfter: jsx(HeaderColumnToggle, null) }; } if (direction === 'row' && (pluginConfig !== null && pluginConfig !== void 0 && pluginConfig.advanced || pluginConfig !== null && pluginConfig !== void 0 && pluginConfig.allowHeaderRow)) { return { key: 'header_row', content: formatMessage(messages.headerRow), value: { name: 'header_row' }, elemAfter: jsx(HeaderRowToggle, null) }; } }; const createRowNumbersMenuItem = () => { return { key: 'row_numbers', content: formatMessage(messages.numberedRows), value: { name: 'row_numbers' }, elemAfter: // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766 jsx("div", { css: toggleStyles }, jsx(Toggle, { id: "toggle-row-numbers", label: formatMessage(messages.numberedRows), onChange: toggleRowNumbers, isChecked: checkIfNumberColumnEnabled(selection) })) }; }; /** * This function is to check if the menu should be closed or not. * As when continously clicking on tyle handle on different rows/columns, * should open the menu corresponding to the position of the drag handle. * @returns true when the menu should be closed, false otherwise */ const shouldCloseMenu = state => { const { isDragMenuOpen: previousOpenState, dragMenuDirection: previousDragMenuDirection, dragMenuIndex: previousDragMenuIndex } = getPluginState(state); // menu open but menu direction changed, means user clicked on drag handle of different row/column // menu open menu direction not changed, but index changed, means user clicked on drag handle of same row/column, different cells. // 2 scenarios above , menu should not be closed. if (previousOpenState === true && previousDragMenuDirection !== direction || previousOpenState === true && previousDragMenuDirection === direction && previousDragMenuIndex !== index) { return false; } else { return true; } }; const closeMenu = (focusTarget = 'handle') => { const { state, dispatch } = editorView; if (shouldCloseMenu(state)) { if (target && focusTarget === 'handle') { target === null || target === void 0 ? void 0 : target.focus(); } else { editorView.focus(); } toggleDragMenu(false, direction, index)(state, dispatch); } }; const handleMenuItemActivated = ({ item }) => { var _menuCallback$item$va; (_menuCallback$item$va = menuCallback[item.value.name]) === null || _menuCallback$item$va === void 0 ? void 0 : _menuCallback$item$va.call(menuCallback, state, dispatch); switch (item.value.name) { case 'background': setIsSubmenuOpen(!isSubmenuOpen); break; case 'header_column': toggleHeaderColumn(); break; case 'header_row': toggleHeaderRow(); break; case 'row_numbers': toggleRowNumbers(); break; default: break; } if (['header_column', 'header_row', 'row_numbers', 'background'].indexOf(item.value.name) <= -1) { closeMenu('editor'); } }; const handleItemMouseEnter = ({ item }) => { var _item$value$name; if (!selectionRect) { return; } if (item.value.name === 'background' && !isSubmenuOpen) { setIsSubmenuOpen(true); } if (!((_item$value$name = item.value.name) !== null && _item$value$name !== void 0 && _item$value$name.startsWith('delete'))) { return; } (item.value.name === 'delete_column' ? hoverColumns(getSelectedColumnIndexes(selectionRect), true) : hoverRows(getSelectedRowIndexes(selectionRect), true))(state, dispatch); }; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any const handleItemMouseLeave = ({ item }) => { if (item.value.name === 'background' && isSubmenuOpen) { setIsSubmenuOpen(false); } if (['sort_column_asc', 'sort_column_desc', 'delete_column', 'delete_row'].indexOf(item.value.name) > -1) { clearHoverSelection()(state, dispatch); } }; useEffect(() => { // focus on first menu item automatically when menu renders // and user is using keyboard if (isOpen && target && isKeyboardModeActive) { const keyboardEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }); target.dispatchEvent(keyboardEvent); } }, [isOpen, target, isKeyboardModeActive]); if (!menuItems) { return null; } if (allowBackgroundColor) { menuItems[1].items.unshift(createBackgroundColorMenuItem()); } // If first row, add toggle for Header row, default is true // If first column, add toggle for Header column, default is false if (index === 0) { if (!fg('platform_editor_enable_table_dnd')) { menuItems.push({ items: [createHeaderRowColumnMenuItemOld(direction)] }); } else if ((pluginConfig !== null && pluginConfig !== void 0 && pluginConfig.advanced || pluginConfig !== null && pluginConfig !== void 0 && pluginConfig.allowHeaderColumn || pluginConfig !== null && pluginConfig !== void 0 && pluginConfig.allowHeaderRow) && fg('platform_editor_enable_table_dnd')) { const headerRowColumnMenuItem = createHeaderRowColumnMenuItem(direction); headerRowColumnMenuItem && menuItems.push({ items: [headerRowColumnMenuItem] }); } } // All rows, add toggle for numbered rows, default is false if (direction === 'row' && (fg('platform_editor_enable_table_dnd') ? (pluginConfig === null || pluginConfig === void 0 ? void 0 : pluginConfig.advanced) || (pluginConfig === null || pluginConfig === void 0 ? void 0 : pluginConfig.allowNumberColumn) : true)) { index === 0 ? menuItems[menuItems.length - 1].items.push(createRowNumbersMenuItem()) : menuItems.push({ items: [createRowNumbersMenuItem()] }); } return jsx(UserIntentPopupWrapper, { api: api, userIntent: "tableDragMenuPopupOpen" }, jsx(DropdownMenu, { disableKeyboardHandling: isSubmenuOpen // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , section: { hasSeparator: true }, items: menuItems, onItemActivated: handleMenuItemActivated, onMouseEnter: handleItemMouseEnter, onMouseLeave: handleItemMouseLeave, handleClose: closeMenu, fitHeight: fitHeight, fitWidth: fitWidth, direction: direction, boundariesElement: boundariesElement, scrollableElement: scrollableElement })); }); export default injectIntl(DragMenu);