UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

297 lines (286 loc) 14.7 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import React from 'react'; import { getTableContainerWidth } from '@atlaskit/editor-common/node-width'; import ReactNodeView from '@atlaskit/editor-common/react-node-view'; import { DOMSerializer } from '@atlaskit/editor-prosemirror/model'; import { akEditorTableNumberColumnWidth } from '@atlaskit/editor-shared-styles'; import { TableMap } from '@atlaskit/editor-tables/table-map'; import { pluginConfig as getPluginConfig } from '../pm-plugins/create-plugin-config'; import { getPluginState } from '../pm-plugins/plugin-factory'; import { pluginKey as tableWidthPluginKey } from '../pm-plugins/table-width'; import { isTableNested } from '../pm-plugins/utils/nodes'; import { TableComponentWithSharedState } from './TableComponentWithSharedState'; import { tableNodeSpecWithFixedToDOM } from './toDOM'; const tableAttributes = node => { return { 'data-number-column': node.attrs.isNumberColumnEnabled, 'data-layout': node.attrs.layout, 'data-autosize': node.attrs.__autoSize, 'data-table-local-id': node.attrs.localId || '', 'data-table-width': node.attrs.width || 'inherit', 'data-table-display-mode': node.attrs.displayMode }; }; const getInlineWidth = (node, options, state, pos, allowTableResizing) => { if (!node.attrs.width && options !== null && options !== void 0 && options.isChromelessEditor || !node.attrs.width && options !== null && options !== void 0 && options.isCommentEditor && allowTableResizing) { return; } // provide a width for tables when custom table width is supported // this is to ensure 'responsive' tables (colgroup widths are undefined) become fixed to // support screen size adjustments const shouldHaveInlineWidth = allowTableResizing && !isTableNested(state, pos); let widthValue = getTableContainerWidth(node); if (node.attrs.isNumberColumnEnabled) { widthValue -= akEditorTableNumberColumnWidth; } return shouldHaveInlineWidth ? widthValue : undefined; }; const handleInlineTableWidth = (table, width) => { if (!table || !width) { return; } table.style.setProperty('width', `${width}px`); }; export default class TableView extends ReactNodeView { constructor(props) { super(props.node, props.view, props.getPos, props.portalProviderAPI, props.eventDispatcher, props, undefined, undefined, // @portal-render-immediately true); _defineProperty(this, "getNode", () => { return this.node; }); _defineProperty(this, "hasHoveredRows", false); this.getPos = props.getPos; this.eventDispatcher = props.eventDispatcher; this.options = props.options; this.getEditorFeatureFlags = props.getEditorFeatureFlags; this.handleRef = node => this._handleTableRef(node); } getContentDOM() { var _this$reactComponentP, _this$reactComponentP2, _this$reactComponentP3, _this$reactComponentP4; const isNested = isTableNested(this.view.state, this.getPos()); const tableDOMStructure = tableNodeSpecWithFixedToDOM({ allowColumnResizing: !!this.reactComponentProps.allowColumnResizing, tableResizingEnabled: !!this.reactComponentProps.allowTableResizing, getEditorContainerWidth: this.reactComponentProps.getEditorContainerWidth, isTableScalingEnabled: (_this$reactComponentP = this.reactComponentProps.options) === null || _this$reactComponentP === void 0 ? void 0 : _this$reactComponentP.isTableScalingEnabled, shouldUseIncreasedScalingPercent: (_this$reactComponentP2 = this.reactComponentProps.options) === null || _this$reactComponentP2 === void 0 ? void 0 : _this$reactComponentP2.shouldUseIncreasedScalingPercent, isCommentEditor: (_this$reactComponentP3 = this.reactComponentProps.options) === null || _this$reactComponentP3 === void 0 ? void 0 : _this$reactComponentP3.isCommentEditor, isChromelessEditor: (_this$reactComponentP4 = this.reactComponentProps.options) === null || _this$reactComponentP4 === void 0 ? void 0 : _this$reactComponentP4.isChromelessEditor, isNested }).toDOM(this.node); const rendered = DOMSerializer.renderSpec(document, tableDOMStructure); if (rendered.dom) { var _this$options, _this$options2, _this$getEditorFeatur; const tableElement = rendered.dom.querySelector('table'); this.table = tableElement ? tableElement : rendered.dom; this.renderedDOM = rendered.dom; if (!((_this$options = this.options) !== null && _this$options !== void 0 && _this$options.isTableScalingEnabled) || (_this$options2 = this.options) !== null && _this$options2 !== void 0 && _this$options2.isTableScalingEnabled && (_this$getEditorFeatur = this.getEditorFeatureFlags) !== null && _this$getEditorFeatur !== void 0 && _this$getEditorFeatur.call(this).tableWithFixedColumnWidthsOption && this.node.attrs.displayMode === 'fixed') { const tableInlineWidth = getInlineWidth(this.node, this.reactComponentProps.options, this.reactComponentProps.view.state, this.reactComponentProps.getPos(), this.reactComponentProps.allowTableResizing); if (tableInlineWidth) { handleInlineTableWidth(this.table, tableInlineWidth); } } } return rendered; } /** * Handles moving the table from ProseMirror's DOM structure into a React-rendered table node. * Temporarily disables mutation observers (except for selection changes) during the move, * preserves selection state, and restores it afterwards if mutations occurred and cursor * wasn't at start of node. This prevents duplicate tables and maintains editor state during * the DOM manipulation. */ _handleTableRef(node) { let oldIgnoreMutation; let selectionBookmark; let mutationsIgnored = false; // Only proceed if we have both a node and table, and the table isn't already inside the node if (node && this.table && !node.contains(this.table)) { // Store the current ignoreMutation handler so we can restore it later oldIgnoreMutation = this.ignoreMutation; // Set up a temporary mutation handler that: // - Ignores all DOM mutations except selection changes // - Tracks when mutations have been ignored via mutationsIgnored flag this.ignoreMutation = m => { const isSelectionMutation = m.type === 'selection'; if (!isSelectionMutation) { mutationsIgnored = true; } return !isSelectionMutation; }; // Store the current selection state if there is a visible selection // This lets us restore it after DOM changes if (this.view.state.selection.visible) { selectionBookmark = this.view.state.selection.getBookmark(); } if (this.dom) { this.dom.setAttribute('data-ssr-placeholder', `table-nodeview-${this.node.attrs.localId}`); this.dom.setAttribute('data-ssr-placeholder-replace', `table-nodeview-${this.node.attrs.localId}`); } // Remove the ProseMirror table DOM structure to avoid duplication, as it's replaced with the React table node. if (this.dom && this.renderedDOM) { this.dom.removeChild(this.renderedDOM); } // Move the table from the ProseMirror table structure into the React rendered table node. node.appendChild(this.table); // After the next frame: requestAnimationFrame(() => { // Restore the original mutation handler this.ignoreMutation = oldIgnoreMutation; // Restore the selection only if: // - We have a selection bookmark // - Mutations were ignored during the table move // - The bookmarked selection is different from the current selection. if (selectionBookmark && mutationsIgnored) { const resolvedSelection = selectionBookmark.resolve(this.view.state.tr.doc); // Don't set the selection if it's the same as the current selection. if (!resolvedSelection.eq(this.view.state.selection)) { const tr = this.view.state.tr.setSelection(resolvedSelection); tr.setMeta('source', 'TableNodeView:_handleTableRef:selection-resync'); this.view.dispatch(tr); } } }); } } setDomAttrs(node) { var _this$options3, _this$options4, _this$getEditorFeatur2; if (!this.table) { return; // width / attribute application to actual table will happen later when table is set } const attrs = tableAttributes(node); Object.keys(attrs).forEach(attr => { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.table.setAttribute(attr, attrs[attr]); }); // Preserve Table Width cannot have inline width set on the table if (!((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.isTableScalingEnabled) || (_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.isTableScalingEnabled && (_this$getEditorFeatur2 = this.getEditorFeatureFlags) !== null && _this$getEditorFeatur2 !== void 0 && _this$getEditorFeatur2.call(this).tableWithFixedColumnWidthsOption && node.attrs.displayMode === 'fixed') { var _tableWidthPluginKey$; // handle inline style when table been resized const tableInlineWidth = getInlineWidth(node, this.reactComponentProps.options, this.view.state, this.getPos(), this.reactComponentProps.allowTableResizing); const isTableResizing = (_tableWidthPluginKey$ = tableWidthPluginKey.getState(this.view.state)) === null || _tableWidthPluginKey$ === void 0 ? void 0 : _tableWidthPluginKey$.resizing; if (!isTableResizing && tableInlineWidth) { handleInlineTableWidth(this.table, tableInlineWidth); } } } render(props, forwardRef) { return /*#__PURE__*/React.createElement(TableComponentWithSharedState, { forwardRef: forwardRef, getNode: this.getNode, view: props.view, options: props.options, eventDispatcher: props.eventDispatcher, api: props.pluginInjectionApi, allowColumnResizing: props.allowColumnResizing, allowTableAlignment: props.allowTableAlignment, allowTableResizing: props.allowTableResizing, allowControls: props.allowControls, getPos: props.getPos, getEditorFeatureFlags: props.getEditorFeatureFlags, dispatchAnalyticsEvent: props.dispatchAnalyticsEvent }); } viewShouldUpdate(nextNode) { const { hoveredRows } = getPluginState(this.view.state); const hoveredRowsChanged = !!(hoveredRows !== null && hoveredRows !== void 0 && hoveredRows.length) !== this.hasHoveredRows; if (nextNode.attrs.isNumberColumnEnabled && hoveredRowsChanged) { this.hasHoveredRows = !!(hoveredRows !== null && hoveredRows !== void 0 && hoveredRows.length); return true; } const node = this.getNode(); if (typeof node.attrs !== typeof nextNode.attrs) { return true; } const attrKeys = Object.keys(node.attrs); const nextAttrKeys = Object.keys(nextNode.attrs); if (attrKeys.length !== nextAttrKeys.length) { return true; } const tableMap = TableMap.get(node); const nextTableMap = TableMap.get(nextNode); if (tableMap.width !== nextTableMap.width) { return true; } return attrKeys.some(key => { return node.attrs[key] !== nextNode.attrs[key]; }); } ignoreMutation(mutation) { const { type, target: { nodeName, firstChild } } = mutation; if (type === 'selection' && (nodeName === null || nodeName === void 0 ? void 0 : nodeName.toUpperCase()) === 'DIV' && (firstChild === null || firstChild === void 0 ? void 0 : firstChild.nodeName.toUpperCase()) === 'TABLE') { return false; } // ED-16668 // Do not remove this fixes an issue with windows firefox that relates to // the addition of the shadow sentinels if (type === 'selection' && (nodeName === null || nodeName === void 0 ? void 0 : nodeName.toUpperCase()) === 'TABLE' && ((firstChild === null || firstChild === void 0 ? void 0 : firstChild.nodeName.toUpperCase()) === 'COLGROUP' || (firstChild === null || firstChild === void 0 ? void 0 : firstChild.nodeName.toUpperCase()) === 'SPAN')) { return false; } if (!this.contentDOM) { return true; } return !this.contentDOM.contains(mutation.target) && mutation.type !== 'selection'; } destroy() { var _this$eventDispatcher; if (this.resizeObserver) { this.resizeObserver.disconnect(); } (_this$eventDispatcher = this.eventDispatcher) === null || _this$eventDispatcher === void 0 ? void 0 : _this$eventDispatcher.emit('TABLE_DELETED', this.node); super.destroy(); } } export const createTableView = (node, view, getPos, portalProviderAPI, eventDispatcher, getEditorContainerWidth, getEditorFeatureFlags, dispatchAnalyticsEvent, pluginInjectionApi, isCommentEditor, isChromelessEditor) => { var _pluginInjectionApi$t; const { pluginConfig, isDragAndDropEnabled, isTableScalingEnabled // same as options.isTableScalingEnabled } = getPluginState(view.state); // Use shared state for isFullWidthModeEnabled and wasFullWidthModeEnabled for most up-to-date values const tableState = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$t = pluginInjectionApi.table) === null || _pluginInjectionApi$t === void 0 ? void 0 : _pluginInjectionApi$t.sharedState.currentState(); const { allowColumnResizing, allowControls, allowTableResizing, allowTableAlignment } = getPluginConfig(pluginConfig); const isTableFixedColumnWidthsOptionEnabled = (getEditorFeatureFlags === null || getEditorFeatureFlags === void 0 ? void 0 : getEditorFeatureFlags().tableWithFixedColumnWidthsOption) || false; const shouldUseIncreasedScalingPercent = isTableScalingEnabled && (isTableFixedColumnWidthsOptionEnabled || isCommentEditor); return new TableView({ node, view, allowColumnResizing, allowTableResizing, allowTableAlignment, allowControls, portalProviderAPI, eventDispatcher, getPos: getPos, options: { isFullWidthModeEnabled: tableState === null || tableState === void 0 ? void 0 : tableState.isFullWidthModeEnabled, wasFullWidthModeEnabled: tableState === null || tableState === void 0 ? void 0 : tableState.wasFullWidthModeEnabled, isDragAndDropEnabled, isTableScalingEnabled, // same as options.isTableScalingEnabled isCommentEditor, isChromelessEditor, shouldUseIncreasedScalingPercent }, getEditorContainerWidth, getEditorFeatureFlags, dispatchAnalyticsEvent, pluginInjectionApi }).init(); };