@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
325 lines (314 loc) • 17.2 kB
JavaScript
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 { TextSelection } from '@atlaskit/editor-prosemirror/state';
import { akEditorTableNumberColumnWidth } from '@atlaskit/editor-shared-styles';
import { TableMap } from '@atlaskit/editor-tables/table-map';
import { fg } from '@atlaskit/platform-feature-flags';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
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, tablesHaveDifferentColumnWidths } 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$reactComponentP5, _this$getEditorFeatur, _this$options, _this$options2;
const tableElement = rendered.dom.querySelector('table');
this.table = tableElement ? tableElement : rendered.dom;
this.renderedDOM = rendered.dom;
const allowFixedColumnWidthOption = (fg('platform_editor_table_fixed_column_width_prop') ? (_this$reactComponentP5 = this.reactComponentProps) === null || _this$reactComponentP5 === void 0 ? void 0 : _this$reactComponentP5.allowFixedColumnWidthOption : (_this$getEditorFeatur = this.getEditorFeatureFlags) === null || _this$getEditorFeatur === void 0 ? void 0 : _this$getEditorFeatur.call(this).tableWithFixedColumnWidthsOption) || false;
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 && allowFixedColumnWidthOption && 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
const {
selection
} = this.view.state;
const tablePos = this.getPos();
if (selection.empty && tablePos && TextSelection.near(this.view.state.doc.resolve(tablePos)).from === selection.from) {
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) {
var _TextSelection$findFr;
const resolvedSelection = selectionBookmark.resolve(this.view.state.tr.doc);
/**
* This handles a very specific case only -> insertion by the user of a new
* table
* Since it's behind a RAF it's possible the user has clicked elsewhere or
* it affects collaborative users (which selection changes shouldn't ever)
*
* This ensures that the selectionBookmark *before* is inside the first
* position in the table and that after it is the text position directly
* before the table
* Ideally we want to remove this RAF entirely but that would require removing
* the DOM manipulation and is a more complex effort
*/
if (!resolvedSelection.eq(this.view.state.selection) && resolvedSelection.empty && // Ensure that the *next* valid text position matches the first position
// in the table
(_TextSelection$findFr = TextSelection.findFrom(this.view.state.doc.resolve(this.view.state.selection.from + 1), 1, true)) !== null && _TextSelection$findFr !== void 0 && _TextSelection$findFr.eq(resolvedSelection)) {
const tr = this.view.state.tr.setSelection(resolvedSelection);
tr.setMeta('source', 'TableNodeView:_handleTableRef:selection-resync');
this.view.dispatch(tr);
}
}
});
}
}
setDomAttrs(node) {
var _this$reactComponentP6, _this$getEditorFeatur2, _this$options3, _this$options4;
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]);
});
const isTableFixedColumnWidthsOptionEnabled = (fg('platform_editor_table_fixed_column_width_prop') ? (_this$reactComponentP6 = this.reactComponentProps) === null || _this$reactComponentP6 === void 0 ? void 0 : _this$reactComponentP6.allowFixedColumnWidthOption : (_this$getEditorFeatur2 = this.getEditorFeatureFlags) === null || _this$getEditorFeatur2 === void 0 ? void 0 : _this$getEditorFeatur2.call(this).tableWithFixedColumnWidthsOption) || false;
// 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 && isTableFixedColumnWidthsOptionEnabled && 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,
allowFixedColumnWidthOption: props.allowFixedColumnWidthOption
});
}
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;
}
if (tablesHaveDifferentColumnWidths(node, nextNode) && expValEquals('platform_editor_lovability_distribute_column_fix', 'isEnabled', true)) {
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, allowFixedColumnWidthOption) => {
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 = (fg('platform_editor_table_fixed_column_width_prop') ? allowFixedColumnWidthOption : 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,
allowFixedColumnWidthOption
}).init();
};