UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

314 lines (304 loc) 15.1 kB
import { ACTION, ACTION_SUBJECT, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics'; import { browser as browserLegacy, getBrowserInfo } from '@atlaskit/editor-common/browser'; import { insideTable } from '@atlaskit/editor-common/core-utils'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { transformSliceToRemoveOpenBodiedExtension, transformSliceToRemoveOpenExpand, transformSliceToRemoveOpenLayoutNodes, transformSliceToRemoveOpenMultiBodiedExtension, transformSliceToRemoveOpenNestedExpand } from '@atlaskit/editor-common/transforms'; import { closestElement } from '@atlaskit/editor-common/utils'; import { findParentDomRefOfType, findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils'; import { TableMap } from '@atlaskit/editor-tables'; import { findTable } from '@atlaskit/editor-tables/utils'; import { fg } from '@atlaskit/platform-feature-flags'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { tableCellView, tableHeaderView, tableRowView, tableView } from '../nodeviews/table-node-views'; import { pluginKey as decorationsPluginKey } from '../pm-plugins/decorations/plugin'; import { TableCssClassName as ClassName } from '../types'; import { handleBlur, handleClick, handleCut, handleFocus, handleMouseDown, handleMouseEnter, handleMouseLeave, handleMouseMove, handleMouseUp, handleTripleClick, whenTableInFocus, withCellTracking } from '../ui/event-handlers'; import { addBoldInEmptyHeaderCells, clearHoverSelection, setTableRef } from './commands'; import { stopKeyboardColumnResizing } from './commands/column-resize'; import { removeResizeHandleDecorations, transformSliceRemoveCellBackgroundColor, transformSliceToFixDarkModeDefaultBackgroundColor, transformSliceToAddTableHeaders, transformSliceToRemoveColumnsWidths } from './commands/misc'; import { defaultHoveredCell, defaultTableSelection } from './default-table-selection'; import { createPluginState, getPluginState } from './plugin-factory'; import { pluginKey } from './plugin-key'; import { fixTables } from './transforms/fix-tables'; import { replaceSelectedTable } from './transforms/replace-table'; import { findControlsHoverDecoration } from './utils/decoration'; import { transformSliceToCorrectEmptyTableCells, transformSliceToFixHardBreakProblemOnCopyFromCell, transformSliceToRemoveOpenTable, transformSliceToRemoveNestedTables, isHeaderRowRequired, transformSliceTableLayoutDefaultToCenter } from './utils/paste'; export const createPlugin = (dispatchAnalyticsEvent, dispatch, portalProviderAPI, nodeViewPortalProviderAPI, eventDispatcher, pluginConfig, getEditorContainerWidth, getEditorFeatureFlags, getIntl, fullWidthModeEnabled, previousFullWidthModeEnabled, dragAndDropEnabled, editorAnalyticsAPI, pluginInjectionApi, isTableScalingEnabled, shouldUseIncreasedScalingPercent, isCommentEditor, isChromelessEditor) => { var _accessibilityUtils; const state = createPluginState(dispatch, { pluginConfig, isTableHovered: false, insertColumnButtonIndex: undefined, insertRowButtonIndex: undefined, isFullWidthModeEnabled: fullWidthModeEnabled, wasFullWidthModeEnabled: previousFullWidthModeEnabled, isHeaderRowEnabled: !!pluginConfig.allowHeaderRow, isHeaderColumnEnabled: false, isDragAndDropEnabled: dragAndDropEnabled, isTableScalingEnabled: isTableScalingEnabled, ...defaultHoveredCell, ...defaultTableSelection, getIntl }); // Used to prevent invalid table cell spans being reported more than once per editor/document const invalidTableIds = []; let editorViewRef = null; const ariaNotifyPlugin = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_accessibilityUtils = pluginInjectionApi.accessibilityUtils) === null || _accessibilityUtils === void 0 ? void 0 : _accessibilityUtils.actions.ariaNotify; const getCurrentEditorState = () => { const editorView = editorViewRef; if (!editorView) { return null; } return editorView.state; }; const getNodeView = () => { return { table: tableView({ portalProviderAPI, eventDispatcher, getEditorContainerWidth, getEditorFeatureFlags, dispatchAnalyticsEvent, pluginInjectionApi, isCommentEditor, isChromelessEditor }), tableRow: tableRowView({ eventDispatcher, pluginInjectionApi }), tableCell: tableCellView({ eventDispatcher, pluginInjectionApi }), tableHeader: tableHeaderView({ eventDispatcher, pluginInjectionApi }) }; }; const nodeViews = getNodeView(); return new SafePlugin({ state: state, key: pluginKey, appendTransaction: (transactions, oldState, newState) => { const tr = transactions.find(tr => tr.getMeta('uiEvent') === 'cut'); function reportInvalidTableCellSpanAttrs(invalidNodeAttr) { if (invalidTableIds.find(id => id === invalidNodeAttr.tableLocalId)) { return; } invalidTableIds.push(invalidNodeAttr.tableLocalId); dispatchAnalyticsEvent({ action: ACTION.INVALID_DOCUMENT_ENCOUNTERED, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL, attributes: { nodeType: invalidNodeAttr.nodeType, reason: `${invalidNodeAttr.attribute}: ${invalidNodeAttr.reason}`, tableLocalId: invalidNodeAttr.tableLocalId, spanValue: invalidNodeAttr.spanValue } }); } if (tr) { const { tableWithFixedColumnWidthsOption = false } = getEditorFeatureFlags(); // "fixTables" removes empty rows as we don't allow that in schema const updatedTr = handleCut(tr, oldState, newState, pluginInjectionApi, editorAnalyticsAPI, editorViewRef || undefined, isTableScalingEnabled, tableWithFixedColumnWidthsOption, shouldUseIncreasedScalingPercent); return fixTables(updatedTr) || updatedTr; } if (transactions.find(tr => tr.docChanged)) { return fixTables(newState.tr, reportInvalidTableCellSpanAttrs); } }, view: editorView => { const domAtPos = editorView.domAtPos.bind(editorView); editorViewRef = editorView; return { update: (view, prevState) => { const { state, dispatch } = view; const { selection } = state; const pluginState = getPluginState(state); let tableRef; if (pluginState.editorHasFocus) { const parent = findParentDomRefOfType(state.schema.nodes.table, domAtPos)(selection); if (parent) { tableRef = // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting parent.querySelector('table') || undefined; } const tableNode = findTable(state.selection); // when keyboard cursor leaves the table we need to stop column resizing const pluginPrevState = getPluginState(prevState); const isStopKeyboardColumResizing = pluginPrevState.isResizeHandleWidgetAdded && pluginPrevState.isKeyboardResize; if (isStopKeyboardColumResizing) { const isTableNodesDifferent = (pluginPrevState === null || pluginPrevState === void 0 ? void 0 : pluginPrevState.tableNode) !== (tableNode === null || tableNode === void 0 ? void 0 : tableNode.node); if (pluginPrevState !== null && pluginPrevState !== void 0 && pluginPrevState.tableNode && tableNode && isTableNodesDifferent) { const oldRowsNumber = TableMap.get(pluginPrevState.tableNode).height; const newRowsNumber = TableMap.get(tableNode.node).height; if (oldRowsNumber !== newRowsNumber || // Add/delete row tableNode.node.attrs.localId !== pluginPrevState.tableNode.attrs.localId) { // Jump to another table stopKeyboardColumnResizing({ ariaNotify: ariaNotifyPlugin, getIntl: getIntl })(state, dispatch); } } else if (!tableNode) { // selection outside of table stopKeyboardColumnResizing({ ariaNotify: ariaNotifyPlugin, getIntl: getIntl })(state, dispatch); } } } if (pluginState.tableRef !== tableRef) { setTableRef(tableRef)(state, dispatch); } if (pluginState.editorHasFocus && pluginState.tableRef) { const { $cursor } = state.selection; if ($cursor) { // Only update bold when it's a cursor const tableCellHeader = findParentNodeOfType(state.schema.nodes.tableHeader)(state.selection); if (tableCellHeader) { addBoldInEmptyHeaderCells(tableCellHeader)(state, dispatch); } } } else if (pluginState.isResizeHandleWidgetAdded) { removeResizeHandleDecorations()(state, dispatch); } } }; }, props: { transformPasted(slice) { const editorState = getCurrentEditorState(); if (!editorState) { return slice; } const { schema } = editorState; // if we're pasting to outside a table or outside a table // header, ensure that we apply any table headers to the first // row of content we see, if required if (!insideTable(editorState) && isHeaderRowRequired(editorState)) { slice = transformSliceToAddTableHeaders(slice, schema); } // This fixes pasting a table with default layout into comment editor // table lose width and expand to full width if (!insideTable(editorState) && isCommentEditor && pluginConfig.allowTableAlignment && isTableScalingEnabled) { slice = transformSliceTableLayoutDefaultToCenter(slice, schema); } slice = transformSliceToFixHardBreakProblemOnCopyFromCell(slice, schema); // We do this separately, so it also applies to drag/drop events // This needs to go before `transformSliceToRemoveOpenExpand` slice = transformSliceToRemoveOpenLayoutNodes(slice, schema); // If a partial paste of expand, paste only the content // This needs to go before `transformSliceToRemoveOpenTable` slice = transformSliceToRemoveOpenExpand(slice, schema); // transformSliceToRemoveOpenTable() transforms based on the depth of the root node, assuming that the tables will be at the root // Bodied extensions will contribute to the depth of the table selection so we need to remove them first /** If a partial paste of bodied extension, paste only text */ slice = transformSliceToRemoveOpenBodiedExtension(slice, schema); /** If a partial paste of table, paste only table's content */ slice = transformSliceToRemoveOpenTable(slice, schema); /** If a partial paste of multi bodied extension, paste only children */ slice = transformSliceToRemoveOpenMultiBodiedExtension(slice, schema); slice = transformSliceToCorrectEmptyTableCells(slice, schema); if (!pluginConfig.allowColumnResizing) { slice = transformSliceToRemoveColumnsWidths(slice, schema); } // If we don't allow background on cells, we need to remove it // from the paste slice if (!pluginConfig.allowBackgroundColor) { slice = transformSliceRemoveCellBackgroundColor(slice, schema); } else { slice = transformSliceToFixDarkModeDefaultBackgroundColor(slice, schema); } slice = transformSliceToRemoveOpenNestedExpand(slice, schema); if (fg('platform_editor_use_nested_table_pm_nodes')) { slice = transformSliceToRemoveNestedTables(slice, schema, editorState.selection); } return slice; }, handleClick: ({ state, dispatch }, _pos, event) => { const decorationSet = decorationsPluginKey.getState(state); const browser = expValEquals('platform_editor_hydratable_ui', 'isEnabled', true) ? getBrowserInfo() : browserLegacy; if (findControlsHoverDecoration(decorationSet).length) { clearHoverSelection()(state, dispatch); } // ED-6069: workaround for Chrome given a regression introduced in prosemirror-view@1.6.8 // Returning true prevents that updateSelection() is getting called in the commit below: // @see https://github.com/ProseMirror/prosemirror-view/commit/33fe4a8b01584f6b4103c279033dcd33e8047b95 if (browser.chrome && event.target) { // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting const targetClassList = event.target.classList; if (targetClassList.contains(ClassName.CONTROLS_BUTTON) || targetClassList.contains(ClassName.CONTEXTUAL_MENU_BUTTON) || targetClassList.contains(ClassName.DRAG_HANDLE_BUTTON_CLICKABLE_ZONE) || targetClassList.contains(ClassName.DRAG_HANDLE_BUTTON_CONTAINER)) { return true; } } return false; }, handleScrollToSelection: view => { // when typing into a sticky header cell, we don't want to scroll // back to the top of the table if the user has already scrolled down const { tableHeader } = view.state.schema.nodes; const domRef = findParentDomRefOfType(tableHeader, view.domAtPos.bind(view))(view.state.selection); const maybeTr = closestElement(domRef, 'tr'); return maybeTr ? maybeTr.classList.contains('sticky') : false; }, handleTextInput: (view, _from, _to, text) => { const { state, dispatch } = view; const { isKeyboardResize } = getPluginState(state); if (isKeyboardResize) { stopKeyboardColumnResizing({ ariaNotify: ariaNotifyPlugin, getIntl: getIntl })(state, dispatch); return false; } const tr = replaceSelectedTable(state, text, INPUT_METHOD.KEYBOARD, editorAnalyticsAPI); if (tr.selectionSet) { dispatch(tr); return true; } return false; }, nodeViews, handleDOMEvents: { focus: handleFocus, blur: handleBlur, mousedown: withCellTracking(handleMouseDown), mouseleave: handleMouseLeave, mousemove: whenTableInFocus(handleMouseMove(nodeViewPortalProviderAPI), pluginInjectionApi), mouseenter: handleMouseEnter, mouseup: whenTableInFocus(handleMouseUp), click: withCellTracking(whenTableInFocus(handleClick)) }, handleTripleClick } }); };