UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

341 lines (331 loc) 19.7 kB
import { INPUT_METHOD, TABLE_STATUS } from '@atlaskit/editor-common/analytics'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { DecorationSet } from '@atlaskit/editor-prosemirror/view'; import { CellSelection } from '@atlaskit/editor-tables/cell-selection'; import { getCellsInRow, getSelectedCellInfo } from '@atlaskit/editor-tables/utils'; import { insm } from '@atlaskit/insm'; import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element'; import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { getPluginState as getTablePluginState } from '../plugin-factory'; import { pluginKey as tablePluginKey } from '../plugin-key'; import { insertColgroupFromNode } from '../table-resizing/utils/colgroup'; import { findNearestCellIndexToPoint } from '../utils/dom'; import { hasMergedCellsInBetween } from '../utils/merged-cells'; import { DragAndDropActionType } from './actions'; import { clearDropTarget, setDropTarget, toggleDragMenu } from './commands'; import { clearDropTargetWithAnalytics, cloneSourceWithAnalytics, moveSourceWithAnalytics } from './commands-with-analytics'; import { DropTargetType } from './consts'; import { createPluginState, getPluginState } from './plugin-factory'; import { pluginKey } from './plugin-key'; import { getDraggableDataFromEvent } from './utils/monitor'; var destroyFn = function destroyFn(editorView, editorAnalyticsAPI, isTableScalingEnabled, isTableFixedColumnWidthsOptionEnabled, isCommentEditor, api) { var editorPageScrollContainer = document.querySelector('.fabric-editor-popup-scroll-parent'); var rowAutoScrollers = editorPageScrollContainer ? [monitorForElements({ canMonitor: function canMonitor(_ref) { var source = _ref.source; var _ref2 = source.data, type = _ref2.type; return type === 'table-row'; }, onDragStart: function onDragStart() { if (expValEquals('cc_editor_interactivity_monitoring', 'isEnabled', true)) { var _insm$session; (_insm$session = insm.session) === null || _insm$session === void 0 || _insm$session.startFeature('tableDragAndDrop'); } // auto scroller doesn't work when scroll-behavior: smooth is set, this monitor temporarily removes it via inline styles // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting editorPageScrollContainer.style.setProperty('scroll-behavior', 'unset'); }, onDrop: function onDrop() { if (expValEquals('cc_editor_interactivity_monitoring', 'isEnabled', true)) { var _insm$session2; (_insm$session2 = insm.session) === null || _insm$session2 === void 0 || _insm$session2.endFeature('tableDragAndDrop'); } // 'null' will remove the inline style // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting editorPageScrollContainer.style.setProperty('scroll-behavior', null); } }), autoScrollForElements({ // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting element: editorPageScrollContainer, canScroll: function canScroll(_ref3) { var source = _ref3.source; var _ref4 = source.data, type = _ref4.type; return type === 'table-row'; } })] : []; return combine.apply(void 0, rowAutoScrollers.concat([monitorForElements({ canMonitor: function canMonitor(_ref5) { var source = _ref5.source; var _ref6 = source.data, type = _ref6.type, localId = _ref6.localId, indexes = _ref6.indexes; // First; Perform any quick checks so we can abort early. if (!indexes || !localId || !(type === 'table-row' || type === 'table-column')) { return false; } var _getTablePluginState = getTablePluginState(editorView.state), tableNode = _getTablePluginState.tableNode; // If the draggable localId is the same as the current selected table localId then we will allow the monitor // watch for changes return localId === (tableNode === null || tableNode === void 0 ? void 0 : tableNode.attrs.localId); }, onDragStart: function onDragStart(_ref7) { var location = _ref7.location; if (expValEquals('cc_editor_interactivity_monitoring', 'isEnabled', true)) { var _insm$session3; (_insm$session3 = insm.session) === null || _insm$session3 === void 0 || _insm$session3.startFeature('tableDragAndDrop'); } toggleDragMenu(false)(editorView.state, editorView.dispatch); if (expValEquals('platform_editor_lovability_user_intent', 'isEnabled', true)) { var _api$userIntent; api === null || api === void 0 || api.core.actions.execute(api === null || api === void 0 || (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.commands.setCurrentUserIntent('dragging')); } }, onDrag: function onDrag(event) { var data = getDraggableDataFromEvent(event); // If no data can be found then it's most like we do not want to perform any drag actions if (!data) { clearDropTarget()(editorView.state, editorView.dispatch); return; } // TODO: ED-26961 - as we drag an element around we are going to want to update the state to acurately reflect the current // insert location as to where the draggable will most likely be go. For example; var sourceType = data.sourceType, targetAdjustedIndex = data.targetAdjustedIndex; var dropTargetType = sourceType === 'table-row' ? DropTargetType.ROW : DropTargetType.COLUMN; var hasMergedCells = hasMergedCellsInBetween([targetAdjustedIndex - 1, targetAdjustedIndex], dropTargetType)(editorView.state.selection); setDropTarget(dropTargetType, targetAdjustedIndex, hasMergedCells)(editorView.state, editorView.dispatch); if (expValEquals('platform_editor_lovability_user_intent', 'isEnabled', true)) { var _api$userIntent2; api === null || api === void 0 || api.core.actions.execute(api === null || api === void 0 || (_api$userIntent2 = api.userIntent) === null || _api$userIntent2 === void 0 ? void 0 : _api$userIntent2.commands.setCurrentUserIntent('dragging')); } }, onDrop: function onDrop(event) { var _cell$row, _cell$col, _api$userIntent3; var data = getDraggableDataFromEvent(event); // On Drop we need to update the table main plugin hoveredCell value with the current row/col that the mouse is // over. This is so the drag handles update their positions to correctly align with the users mouse. Unfortunately // at this point in time and during the drag opertation, the drop targets are eating all the mouse events so // it's not possible to know what row/col the mouse is over (via mouse events). This attempts to locate the nearest cell and // then tries to update the main table hoveredCell value by piggy-backing the transaction onto the command // triggered by this on drop event. var _getTablePluginState2 = getTablePluginState(editorView.state), hoveredCell = _getTablePluginState2.hoveredCell; var cell = findNearestCellIndexToPoint(event.location.current.input.clientX, event.location.current.input.clientY); var tr = editorView.state.tr; var action = { type: 'HOVER_CELL', data: { hoveredCell: { rowIndex: (_cell$row = cell === null || cell === void 0 ? void 0 : cell.row) !== null && _cell$row !== void 0 ? _cell$row : hoveredCell.rowIndex, colIndex: (_cell$col = cell === null || cell === void 0 ? void 0 : cell.col) !== null && _cell$col !== void 0 ? _cell$col : hoveredCell.colIndex } } }; tr.setMeta(tablePluginKey, action); if (expValEquals('platform_editor_lovability_user_intent', 'isEnabled', true) && (api === null || api === void 0 || (_api$userIntent3 = api.userIntent) === null || _api$userIntent3 === void 0 || (_api$userIntent3 = _api$userIntent3.sharedState.currentState()) === null || _api$userIntent3 === void 0 ? void 0 : _api$userIntent3.currentUserIntent) === 'dragging') { var _api$userIntent4; api === null || api === void 0 || api.core.actions.execute(api === null || api === void 0 || (_api$userIntent4 = api.userIntent) === null || _api$userIntent4 === void 0 ? void 0 : _api$userIntent4.commands.setCurrentUserIntent('default')); } // If no data can be found then it's most like we do not want to perform any drop action if (!data) { var _event$source, _event$source2; // If we're able to determine the source type of the dropped element then we should report to analytics that // the drop event was cancelled. Otherwise we will cancel silently. if (expValEquals('cc_editor_interactivity_monitoring', 'isEnabled', true)) { var _insm$session4; (_insm$session4 = insm.session) === null || _insm$session4 === void 0 || _insm$session4.endFeature('tableDragAndDrop'); } if ((event === null || event === void 0 || (_event$source = event.source) === null || _event$source === void 0 || (_event$source = _event$source.data) === null || _event$source === void 0 ? void 0 : _event$source.type) === 'table-row' || (event === null || event === void 0 || (_event$source2 = event.source) === null || _event$source2 === void 0 || (_event$source2 = _event$source2.data) === null || _event$source2 === void 0 ? void 0 : _event$source2.type) === 'table-column') { var _event$source$data; return clearDropTargetWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.DRAG_AND_DROP, event.source.data.type, (_event$source$data = event.source.data) === null || _event$source$data === void 0 ? void 0 : _event$source$data.indexes, TABLE_STATUS.CANCELLED, tr)(editorView.state, editorView.dispatch); } return clearDropTarget(tr)(editorView.state, editorView.dispatch); } var sourceType = data.sourceType, sourceIndexes = data.sourceIndexes, targetIndex = data.targetIndex, targetAdjustedIndex = data.targetAdjustedIndex, targetDirection = data.targetDirection, direction = data.direction, behaviour = data.behaviour; // When we drop on a target we will know the targets row/col index for certain, if (sourceType === 'table-row') { action.data.hoveredCell.rowIndex = targetIndex; } else { action.data.hoveredCell.colIndex = targetIndex; } // If the drop target index contains merged cells then we should not allow the drop to occur. if (hasMergedCellsInBetween([targetAdjustedIndex - 1, targetAdjustedIndex], sourceType === 'table-row' ? DropTargetType.ROW : DropTargetType.COLUMN)(editorView.state.selection)) { clearDropTargetWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.DRAG_AND_DROP, sourceType, sourceIndexes, // This event is mrked as invalid because the user is attempting to drop an element in an area which has merged cells. TABLE_STATUS.INVALID, tr)(editorView.state, editorView.dispatch); if (expValEquals('cc_editor_interactivity_monitoring', 'isEnabled', true)) { var _insm$session5; (_insm$session5 = insm.session) === null || _insm$session5 === void 0 || _insm$session5.endFeature('tableDragAndDrop'); } return; } requestAnimationFrame(function () { if (behaviour === 'clone') { cloneSourceWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.DRAG_AND_DROP, sourceType, sourceIndexes, targetIndex, targetDirection, tr)(editorView.state, editorView.dispatch); } else { moveSourceWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.DRAG_AND_DROP, sourceType, sourceIndexes, targetAdjustedIndex + (direction === 1 ? -1 : 0), tr)(editorView.state, editorView.dispatch); } // force a colgroup update here, otherwise dropped columns don't have // the correct width immediately after the drop if (sourceType === 'table-column') { var _getTablePluginState3 = getTablePluginState(editorView.state), tableRef = _getTablePluginState3.tableRef, tableNode = _getTablePluginState3.tableNode; if (tableRef && tableNode) { var isTableScalingEnabledOnCurrentTable = isTableScalingEnabled; var isTableScalingWithFixedColumnWidthsOptionEnabled = isTableScalingEnabled && isTableFixedColumnWidthsOptionEnabled; if (isTableScalingWithFixedColumnWidthsOptionEnabled) { isTableScalingEnabledOnCurrentTable = tableNode.attrs.displayMode !== 'fixed'; } if (isTableScalingEnabled && isCommentEditor) { isTableScalingEnabledOnCurrentTable = true; } var shouldUseIncreasedScalingPercent = isTableScalingWithFixedColumnWidthsOptionEnabled || isTableScalingEnabled && isCommentEditor; insertColgroupFromNode(tableRef, tableNode, isTableScalingEnabledOnCurrentTable, undefined, shouldUseIncreasedScalingPercent, isCommentEditor); } } editorView.focus(); if (expValEquals('cc_editor_interactivity_monitoring', 'isEnabled', true)) { var _insm$session6; (_insm$session6 = insm.session) === null || _insm$session6 === void 0 || _insm$session6.endFeature('tableDragAndDrop'); } }); } })])); }; export var createPlugin = function createPlugin(dispatch, editorAnalyticsAPI) { var isTableScalingEnabled = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; var isTableFixedColumnWidthsOptionEnabled = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; var isCommentEditor = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false; var api = arguments.length > 5 ? arguments[5] : undefined; return new SafePlugin({ state: createPluginState(dispatch, function (state) { return { decorationSet: DecorationSet.empty, dropTargetType: DropTargetType.NONE, dropTargetIndex: 0, isDragMenuOpen: false, dragMenuIndex: 0, isDragging: false, isKeyboardModeActive: false }; }), key: pluginKey, appendTransaction: function appendTransaction(transactions, oldState, newState) { var _getTablePluginState4 = getTablePluginState(oldState), oldTargetCellPosition = _getTablePluginState4.targetCellPosition; var _getTablePluginState5 = getTablePluginState(newState), newTargetCellPosition = _getTablePluginState5.targetCellPosition; var _getPluginState = getPluginState(newState), _getPluginState$isDra = _getPluginState.isDragMenuOpen, isDragMenuOpen = _getPluginState$isDra === void 0 ? false : _getPluginState$isDra, dragMenuIndex = _getPluginState.dragMenuIndex; transactions.forEach(function (transaction) { if (transaction.getMeta('selectedRowViaKeyboard')) { var button = document.querySelector('#drag-handle-button-row'); if (button) { button.focus(); } } if (transaction.getMeta('selectedColumnViaKeyboard')) { var _button = document.querySelector('#drag-handle-button-column'); if (_button) { _button.focus(); } } }); // What's happening here? you asked... In a nutshell; // If the target cell position changes while the drag menu is open then we want to close the drag menu if it has been opened. // This will stop the drag menu from moving around the screen to different row/cols. Too achieve this we need // to check if the new target cell position is pointed at a different cell than what the drag menu was opened on. if (oldTargetCellPosition !== newTargetCellPosition) { if (isDragMenuOpen) { var tr = newState.tr; var action = { type: DragAndDropActionType.TOGGLE_DRAG_MENU, data: { isDragMenuOpen: false, direction: undefined } }; if (newTargetCellPosition !== undefined) { var cells = getCellsInRow(dragMenuIndex)(tr.selection); // TODO: ED-20673 - check if it is a cell selection, // when true, a drag handle is clicked and isDragMenuOpen is true here // should not close the drag menu. var isCellSelection = tr.selection instanceof CellSelection; if (cells && cells.length && cells[0].node !== tr.doc.nodeAt(newTargetCellPosition) && !isCellSelection) { return tr.setMeta(pluginKey, action); } // else NOP } else { return tr.setMeta(pluginKey, action); } } } }, view: function view(editorView) { return { destroy: destroyFn(editorView, editorAnalyticsAPI, isTableScalingEnabled, isTableFixedColumnWidthsOptionEnabled, isCommentEditor, api) }; }, props: { decorations: function decorations(state) { var _getPluginState2 = getPluginState(state), decorationSet = _getPluginState2.decorationSet; return decorationSet; }, handleKeyDown: function handleKeyDown(view, event) { var _ref8; var tr = view.state.tr; var keysToTrapWhen = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']; /** fix for NCS spam update where the user is holding down the move column / row keyboard shortcut * if the user is holding down shortcut (ctrl + shift + alt + arrowKey), we want to move the selection only once * See ticket ED-22154 https://product-fabric.atlassian.net/browse/ED-22154 */ // Do early check for the keys we want to trap here so we can abort early if (event.ctrlKey && event.shiftKey && event.altKey) { var _getSelectedCellInfo = getSelectedCellInfo(tr.selection), verticalCells = _getSelectedCellInfo.verticalCells, horizontalCells = _getSelectedCellInfo.horizontalCells, totalRowCount = _getSelectedCellInfo.totalRowCount, totalColumnCount = _getSelectedCellInfo.totalColumnCount; var isRowOrColumnSelected = horizontalCells === totalColumnCount || verticalCells === totalRowCount; if (isRowOrColumnSelected && keysToTrapWhen.includes(event.key) && event.repeat) { return true; } } var isDragHandleFocused = ['drag-handle-button-row', 'drag-handle-button-column' // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting ].includes((_ref8 = event.target || null) === null || _ref8 === void 0 ? void 0 : _ref8.id); var keysToTrap = ['Enter', ' ']; var _getPluginState3 = getPluginState(view.state), _getPluginState3$isDr = _getPluginState3.isDragMenuOpen, isDragMenuOpen = _getPluginState3$isDr === void 0 ? false : _getPluginState3$isDr; // drag handle is focused, and user presses any key return them back to editing if (isDragHandleFocused && !isDragMenuOpen && !keysToTrap.includes(event.key)) { view.dom.focus(); return true; } if (isDragHandleFocused && keysToTrap.includes(event.key) || isDragMenuOpen && keysToTrapWhen.includes(event.key)) { return true; } } } }); };