@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
588 lines (581 loc) • 23.1 kB
JavaScript
/* 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);