@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
341 lines (331 loc) • 19.7 kB
JavaScript
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;
}
}
}
});
};