UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

493 lines (482 loc) 25 kB
import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; import rafSchedule from 'raf-schd'; import { ACTION_SUBJECT, EVENT_TYPE, TABLE_ACTION } from '@atlaskit/editor-common/analytics'; import { getBrowserInfo } from '@atlaskit/editor-common/browser'; import { getParentOfTypeCount } from '@atlaskit/editor-common/nesting'; import { closestElement, isElementInTableCell, isLastItemMediaGroup, setNodeSelection } from '@atlaskit/editor-common/utils'; import { Selection, TextSelection } from '@atlaskit/editor-prosemirror/state'; import { findParentNodeOfTypeClosestToPos } from '@atlaskit/editor-prosemirror/utils'; import { CellSelection } from '@atlaskit/editor-tables/cell-selection'; import { TableMap } from '@atlaskit/editor-tables/table-map'; import { cellAround, findCellRectClosestToPos, findTable, getSelectionRect, removeTable } from '@atlaskit/editor-tables/utils'; import { addResizeHandleDecorations, clearHoverSelection, hideInsertColumnOrRowButton, hideResizeHandleLine, hoverCell, hoverColumns, selectColumn, setEditorFocus, setTableHovered, showInsertColumnButton, showInsertRowButton, showResizeHandleLine } from '../pm-plugins/commands'; import { getPluginState as getDragDropPluginState } from '../pm-plugins/drag-and-drop/plugin-factory'; import { getPluginState } from '../pm-plugins/plugin-factory'; import { getPluginState as getResizePluginState } from '../pm-plugins/table-resizing/plugin-factory'; import { deleteColumns } from '../pm-plugins/transforms/delete-columns'; import { deleteRows } from '../pm-plugins/transforms/delete-rows'; import { getSelectedCellInfo } from '../pm-plugins/utils/analytics'; import { convertHTMLCellIndexToColumnIndex, getColumnIndexMappedToColumnIndexInFirstRow } from '../pm-plugins/utils/column-controls'; import { getColumnOrRowIndex, getMousePositionHorizontalRelativeByElement, getMousePositionVerticalRelativeByElement, hasResizeHandler, isCell, isColumnControlsDecorations, isCornerButton, isDragColumnFloatingInsertDot, isDragCornerButton, isDragRowFloatingInsertDot, isInsertRowButton, isResizeHandleDecoration, isRowControlsButton, isTableContainerOrWrapper, isTableControlsButton } from '../pm-plugins/utils/dom'; import { getAllowAddColumnCustomStep } from '../pm-plugins/utils/get-allow-add-column-custom-step'; import { TableCssClassName as ClassName, RESIZE_HANDLE_AREA_DECORATION_GAP } from '../types'; var isFocusingCalendar = function isFocusingCalendar(event) { return event instanceof FocusEvent && event.relatedTarget instanceof HTMLElement && event.relatedTarget.getAttribute('aria-label') === 'calendar'; }; var isFocusingModal = function isFocusingModal(event) { return event instanceof FocusEvent && event.relatedTarget instanceof HTMLElement && event.relatedTarget.closest('[role="dialog"]'); }; var isFocusingFloatingToolbar = function isFocusingFloatingToolbar(event) { return event instanceof FocusEvent && event.relatedTarget instanceof HTMLElement && event.relatedTarget.closest('[role="toolbar"]'); }; var isFocusingDragHandles = function isFocusingDragHandles(event) { return event instanceof FocusEvent && event.relatedTarget instanceof HTMLElement && event.relatedTarget.closest('button') && event.relatedTarget.getAttribute('draggable') === 'true'; }; var isFocusingDragHandlesClickableZone = function isFocusingDragHandlesClickableZone(event) { return event instanceof FocusEvent && event.relatedTarget instanceof HTMLElement && event.relatedTarget.closest('button') && event.relatedTarget.classList.contains(ClassName.DRAG_HANDLE_BUTTON_CLICKABLE_ZONE); }; export var handleBlur = function handleBlur(view, event) { var state = view.state, dispatch = view.dispatch; // IE version check for ED-4665 // Calendar focus check for ED-10466 if (getBrowserInfo().ie_version !== 11 && !isFocusingCalendar(event) && !isFocusingModal(event) && !isFocusingFloatingToolbar(event) && !isFocusingDragHandles(event) && !isFocusingDragHandlesClickableZone(event)) { setEditorFocus(false)(state, dispatch); } event.preventDefault(); return false; }; export var handleFocus = function handleFocus(view, event) { var state = view.state, dispatch = view.dispatch; setEditorFocus(true)(state, dispatch); event.preventDefault(); return false; }; export var handleClick = function handleClick(view, event) { if (!(event.target instanceof HTMLElement)) { return false; } var element = event.target; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion var table = findTable(view.state.selection); // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting if (event instanceof MouseEvent && isColumnControlsDecorations(element)) { // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting var _getColumnOrRowIndex = getColumnOrRowIndex(element), _getColumnOrRowIndex2 = _slicedToArray(_getColumnOrRowIndex, 1), startIndex = _getColumnOrRowIndex2[0]; var state = view.state, _dispatch = view.dispatch; return selectColumn(startIndex, event.shiftKey)(state, _dispatch); } var matchfn = element.matches ? element.matches : element.msMatchesSelector; // check if the table cell with an image is clicked and its not the image itself if (!table || // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting !isElementInTableCell(element) || !matchfn || matchfn.call(element, 'table .image, table p, table .image div')) { return false; } var map = TableMap.get(table.node); /** Getting the offset of current item clicked */ // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting var colElement = closestElement(element, 'td') || // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting closestElement(element, 'th'); var colIndex = colElement && colElement.cellIndex; // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting var rowElement = closestElement(element, 'tr'); var rowIndex = rowElement && rowElement.rowIndex; var cellIndex = map.width * rowIndex + colIndex; var dispatch = view.dispatch, _view$state = view.state, tr = _view$state.tr, paragraph = _view$state.schema.nodes.paragraph; var cellPos = map.map[cellIndex]; if (isNaN(cellPos) || cellPos === undefined || typeof cellPos !== 'number') { return false; } var editorElement = table.node.nodeAt(cellPos); /** Only if the last item is media group, insert a paragraph */ if (isLastItemMediaGroup(editorElement)) { var posInTable = map.map[cellIndex] + editorElement.nodeSize; tr.insert(posInTable + table.pos, paragraph.create()); dispatch(tr); setNodeSelection(view, posInTable + table.pos); } return true; }; export var handleMouseOver = function handleMouseOver(view, mouseEvent) { if (!(mouseEvent.target instanceof HTMLElement)) { return false; } var state = view.state, dispatch = view.dispatch; var target = mouseEvent.target; var _getPluginState = getPluginState(state), insertColumnButtonIndex = _getPluginState.insertColumnButtonIndex, insertRowButtonIndex = _getPluginState.insertRowButtonIndex, isTableHovered = _getPluginState.isTableHovered; if (isInsertRowButton(target)) { var _getColumnOrRowIndex3 = getColumnOrRowIndex(target), _getColumnOrRowIndex4 = _slicedToArray(_getColumnOrRowIndex3, 2), startIndex = _getColumnOrRowIndex4[0], endIndex = _getColumnOrRowIndex4[1]; var positionRow = getMousePositionVerticalRelativeByElement(mouseEvent) === 'bottom' ? endIndex : startIndex; return showInsertRowButton(positionRow)(state, dispatch); } if (isColumnControlsDecorations(target)) { var _getColumnOrRowIndex5 = getColumnOrRowIndex(target), _getColumnOrRowIndex6 = _slicedToArray(_getColumnOrRowIndex5, 1), _startIndex = _getColumnOrRowIndex6[0]; var _state = view.state, _dispatch2 = view.dispatch; return hoverColumns([_startIndex], false)(_state, _dispatch2); } var isNestedTable = getParentOfTypeCount(state.schema.nodes.table)(state.selection.$from) > 1; if (isNestedTable) { // if the table is nested inside a table, we only call hideInsertColumnOrRowButton if the table nearest to the mouse target is NOT the parent table var nearestTable = closestElement(target, 'table'); var nestedTable = findParentNodeOfTypeClosestToPos(state.doc.resolve(state.selection.from), [state.schema.nodes.table]); var parentTable = findParentNodeOfTypeClosestToPos(state.doc.resolve((nestedTable === null || nestedTable === void 0 ? void 0 : nestedTable.pos) || 0), [state.schema.nodes.table]); if ((nearestTable === null || nearestTable === void 0 ? void 0 : nearestTable.dataset.tableLocalId) !== (parentTable === null || parentTable === void 0 ? void 0 : parentTable.node.attrs.localId) && (isCell(target) || isCornerButton(target)) && (typeof insertColumnButtonIndex === 'number' || typeof insertRowButtonIndex === 'number')) { return hideInsertColumnOrRowButton()(state, dispatch); } } else if ((isCell(target) || isCornerButton(target)) && (typeof insertColumnButtonIndex === 'number' || typeof insertRowButtonIndex === 'number')) { return hideInsertColumnOrRowButton()(state, dispatch); } if (isResizeHandleDecoration(target)) { var _getColumnOrRowIndex7 = getColumnOrRowIndex(target), _getColumnOrRowIndex8 = _slicedToArray(_getColumnOrRowIndex7, 2), _startIndex2 = _getColumnOrRowIndex8[0], _endIndex = _getColumnOrRowIndex8[1]; return showResizeHandleLine({ left: _startIndex2, right: _endIndex })(state, dispatch); } if (!isTableHovered) { return setTableHovered(true)(state, dispatch); } return false; }; export var handleMouseUp = function handleMouseUp(view, mouseEvent) { if (!(mouseEvent instanceof MouseEvent)) { return false; } var state = view.state, dispatch = view.dispatch; var _getPluginState2 = getPluginState(state), insertColumnButtonIndex = _getPluginState2.insertColumnButtonIndex, tableNode = _getPluginState2.tableNode, tableRef = _getPluginState2.tableRef; if (insertColumnButtonIndex !== undefined && tableRef && tableRef.parentElement && tableNode) { var _TableMap$get = TableMap.get(tableNode), width = _TableMap$get.width; var newInsertColumnButtonIndex = insertColumnButtonIndex + 1; if (width === newInsertColumnButtonIndex) { var tableWidth = tableRef.clientWidth; tableRef.parentElement.scrollTo(tableWidth, 0); return showInsertColumnButton(newInsertColumnButtonIndex)(state, dispatch); } } return false; }; // Ignore any `mousedown` `event` from control and numbered column buttons // PM end up changing selection during shift selection if not prevented export var handleMouseDown = function handleMouseDown(_, event) { var isControl = !!(event.target && event.target instanceof HTMLElement && (isTableContainerOrWrapper(event.target) || isColumnControlsDecorations(event.target) || isRowControlsButton(event.target) || isDragCornerButton(event.target))); if (isControl) { event.preventDefault(); } return isControl; }; export var handleMouseOut = function handleMouseOut(view, mouseEvent) { if (!(mouseEvent instanceof MouseEvent) || !(mouseEvent.target instanceof HTMLElement)) { return false; } var target = mouseEvent.target; if (isColumnControlsDecorations(target)) { var state = view.state, dispatch = view.dispatch; return clearHoverSelection()(state, dispatch); } var relatedTarget = mouseEvent.relatedTarget; // In case the user is moving between cell at the same column // we don't need to hide the resize handle decoration if (isResizeHandleDecoration(target) && !isResizeHandleDecoration(relatedTarget)) { var _state2 = view.state, _dispatch3 = view.dispatch; var _getPluginState3 = getPluginState(_state2), isKeyboardResize = _getPluginState3.isKeyboardResize; if (isKeyboardResize) { // no need to hide decoration if column resizing started by keyboard return false; } return hideResizeHandleLine()(_state2, _dispatch3); } return false; }; export var handleMouseEnter = function handleMouseEnter(view, mouseEvent) { var state = view.state, dispatch = view.dispatch; var _getPluginState4 = getPluginState(state), isTableHovered = _getPluginState4.isTableHovered; if (!isTableHovered) { return setTableHovered(true)(state, dispatch); } return false; }; export var handleMouseLeave = function handleMouseLeave(view, event) { if (!(event.target instanceof HTMLElement)) { return false; } var state = view.state, dispatch = view.dispatch; var _getPluginState5 = getPluginState(state), insertColumnButtonIndex = _getPluginState5.insertColumnButtonIndex, insertRowButtonIndex = _getPluginState5.insertRowButtonIndex, isDragAndDropEnabled = _getPluginState5.isDragAndDropEnabled, isTableHovered = _getPluginState5.isTableHovered; if (isTableHovered) { if (isDragAndDropEnabled) { var _getDragDropPluginSta = getDragDropPluginState(state), _getDragDropPluginSta2 = _getDragDropPluginSta.isDragMenuOpen, isDragMenuOpen = _getDragDropPluginSta2 === void 0 ? false : _getDragDropPluginSta2; !isDragMenuOpen && setTableHovered(false)(state, dispatch); } else { setTableHovered(false)(state, dispatch); } return true; } // If this table doesn't have focus then we want to skip everything after this. if (!isTableInFocus(view)) { return false; } var target = event.target; if (isTableControlsButton(target)) { return true; } if ((typeof insertColumnButtonIndex !== 'undefined' || typeof insertRowButtonIndex !== 'undefined') && hideInsertColumnOrRowButton()(state, dispatch)) { return true; } return false; }; // IMPORTANT: The mouse move handler has been setup with RAF schedule to avoid Reflows which will occur as some methods // need to access the mouse event offset position and also the target clientWidth vallue. var handleMouseMoveDebounce = function handleMouseMoveDebounce(nodeViewPortalProviderAPI) { return rafSchedule(function (view, event, offsetX) { if (!(event.target instanceof HTMLElement)) { return false; } var element = event.target; if (isColumnControlsDecorations(element) || isDragColumnFloatingInsertDot(element)) { var state = view.state, dispatch = view.dispatch; var _getPluginState6 = getPluginState(state), insertColumnButtonIndex = _getPluginState6.insertColumnButtonIndex; var _getColumnOrRowIndex9 = getColumnOrRowIndex(element), _getColumnOrRowIndex0 = _slicedToArray(_getColumnOrRowIndex9, 2), startIndex = _getColumnOrRowIndex0[0], endIndex = _getColumnOrRowIndex0[1]; var positionColumn = getMousePositionHorizontalRelativeByElement(event, offsetX, undefined) === 'right' ? endIndex : startIndex; if (positionColumn !== insertColumnButtonIndex) { return showInsertColumnButton(positionColumn)(state, dispatch); } } if (isRowControlsButton(element) || isDragRowFloatingInsertDot(element)) { var _state3 = view.state, _dispatch4 = view.dispatch; var _getPluginState7 = getPluginState(_state3), insertRowButtonIndex = _getPluginState7.insertRowButtonIndex; var _getColumnOrRowIndex1 = getColumnOrRowIndex(element), _getColumnOrRowIndex10 = _slicedToArray(_getColumnOrRowIndex1, 2), _startIndex3 = _getColumnOrRowIndex10[0], _endIndex2 = _getColumnOrRowIndex10[1]; var positionRow = getMousePositionVerticalRelativeByElement(event) === 'bottom' ? _endIndex2 : _startIndex3; if (positionRow !== insertRowButtonIndex) { return showInsertRowButton(positionRow)(_state3, _dispatch4); } } if (!isResizeHandleDecoration(element) && isCell(element)) { var _positionColumn = getMousePositionHorizontalRelativeByElement(event, offsetX, RESIZE_HANDLE_AREA_DECORATION_GAP); if (_positionColumn !== null) { var _state4 = view.state, _dispatch5 = view.dispatch; var _getPluginState8 = getPluginState(_state4), resizeHandleColumnIndex = _getPluginState8.resizeHandleColumnIndex, resizeHandleRowIndex = _getPluginState8.resizeHandleRowIndex; var isKeyboardResize = getPluginState(_state4).isKeyboardResize; var tableCell = closestElement(element, 'td, th'); var cellStartPosition = view.posAtDOM(tableCell, 0); var rect = findCellRectClosestToPos(_state4.doc.resolve(cellStartPosition)); if (rect) { var columnEndIndexTarget = _positionColumn === 'left' ? rect.left : rect.right; var rowIndexTarget = rect.top; if ((columnEndIndexTarget !== resizeHandleColumnIndex || rowIndexTarget !== resizeHandleRowIndex || !hasResizeHandler({ target: element, columnEndIndexTarget: columnEndIndexTarget })) && !isKeyboardResize // if initiated by keyboard don't need to react on hover for other resize sliders ) { return addResizeHandleDecorations(rowIndexTarget, columnEndIndexTarget, true, nodeViewPortalProviderAPI)(_state4, _dispatch5); } } } } return false; }); }; export var handleMouseMove = function handleMouseMove(nodeViewPortalProviderAPI) { return function (view, event) { if (!(event.target instanceof HTMLElement)) { return false; } // NOTE: When accessing offsetX in gecko from a deferred callback, it will return 0. However it will be non-zero if accessed // within the scope of it's initial mouse move handler. Also Chrome does return the correct value, however it could trigger // a reflow. So for now this will just grab the offsetX value immediately for gecko and chrome will calculate later // in the deferred callback handler. // Bug Tracking: https://bugzilla.mozilla.org/show_bug.cgi?id=1882903 handleMouseMoveDebounce(nodeViewPortalProviderAPI)(view, event, getBrowserInfo().gecko ? event.offsetX : NaN); return false; }; }; export function handleTripleClick(view, pos) { var state = view.state, dispatch = view.dispatch; var $cellPos = cellAround(state.doc.resolve(pos)); if (!$cellPos) { return false; } var cell = state.doc.nodeAt($cellPos.pos); if (cell) { var selFrom = Selection.findFrom($cellPos, 1, true); var selTo = Selection.findFrom(state.doc.resolve($cellPos.pos + cell.nodeSize), -1, true); if (selFrom && selTo) { dispatch(state.tr.setSelection(new TextSelection(selFrom.$from, selTo.$to))); return true; } } return false; } export var handleCut = function handleCut(oldTr, oldState, newState, api, editorAnalyticsAPI, editorView) { var isTableScalingEnabled = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : false; var isTableFixedColumnWidthsOptionEnabled = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : false; var shouldUseIncreasedScalingPercent = arguments.length > 8 && arguments[8] !== undefined ? arguments[8] : false; var oldSelection = oldState.tr.selection; var tr = newState.tr; if (oldSelection instanceof CellSelection) { var $anchorCell = oldTr.doc.resolve(oldTr.mapping.map(oldSelection.$anchorCell.pos)); var $headCell = oldTr.doc.resolve(oldTr.mapping.map(oldSelection.$headCell.pos)); var cellSelection = new CellSelection($anchorCell, $headCell); tr.setSelection(cellSelection); if (tr.selection instanceof CellSelection) { var rect = getSelectionRect(cellSelection); if (rect) { var _getSelectedCellInfo = getSelectedCellInfo(tr.selection), verticalCells = _getSelectedCellInfo.verticalCells, horizontalCells = _getSelectedCellInfo.horizontalCells, totalCells = _getSelectedCellInfo.totalCells, totalRowCount = _getSelectedCellInfo.totalRowCount, totalColumnCount = _getSelectedCellInfo.totalColumnCount; // Reassigning to make it more obvious and consistent editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent({ action: TABLE_ACTION.CUT, actionSubject: ACTION_SUBJECT.TABLE, actionSubjectId: null, attributes: { verticalCells: verticalCells, horizontalCells: horizontalCells, totalCells: totalCells, totalRowCount: totalRowCount, totalColumnCount: totalColumnCount }, eventType: EVENT_TYPE.TRACK })(tr); // Need this check again since we are overriding the tr in previous statement if (tr.selection instanceof CellSelection) { var isTableSelected = tr.selection.isRowSelection() && tr.selection.isColSelection(); if (isTableSelected) { tr = removeTable(tr); } else if (tr.selection.isRowSelection()) { var _getPluginState9 = getPluginState(newState), isHeaderRowRequired = _getPluginState9.pluginConfig.isHeaderRowRequired; tr = deleteRows(rect, isHeaderRowRequired)(tr); } else if (tr.selection.isColSelection()) { tr = deleteColumns(rect, getAllowAddColumnCustomStep(oldState), api, editorView, isTableScalingEnabled, isTableFixedColumnWidthsOptionEnabled, shouldUseIncreasedScalingPercent)(tr); } } } } } return tr; }; export var isTableInFocus = function isTableInFocus(view) { var _getPluginState0, _getResizePluginState; return !!((_getPluginState0 = getPluginState(view.state)) !== null && _getPluginState0 !== void 0 && _getPluginState0.tableNode) && !((_getResizePluginState = getResizePluginState(view.state)) !== null && _getResizePluginState !== void 0 && _getResizePluginState.dragging); }; export var whenTableInFocus = function whenTableInFocus(eventHandler, pluginInjectionApi) { return function (view, mouseEvent) { var _pluginInjectionApi$e; if (!isTableInFocus(view)) { return false; } var isViewMode = (pluginInjectionApi === null || pluginInjectionApi === void 0 || (_pluginInjectionApi$e = pluginInjectionApi.editorViewMode) === null || _pluginInjectionApi$e === void 0 || (_pluginInjectionApi$e = _pluginInjectionApi$e.sharedState.currentState()) === null || _pluginInjectionApi$e === void 0 ? void 0 : _pluginInjectionApi$e.mode) === 'view'; /** * Table cannot be in focus if we are in view mode. * This will prevent an infinite flow of adding and removing * resize handle decorations in view mode that causes unpredictable * selections. */ if (isViewMode) { return false; } return eventHandler(view, mouseEvent); }; }; var trackCellLocation = function trackCellLocation(view, mouseEvent) { var _tableElement$dataset; var target = mouseEvent.target; // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting var maybeTableCell = isElementInTableCell(target); var _getPluginState1 = getPluginState(view.state), tableNode = _getPluginState1.tableNode, tableRef = _getPluginState1.tableRef; // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting var tableElement = closestElement(target, 'table'); // hover will only trigger if target localId is the same with selected localId if (tableElement !== null && tableElement !== void 0 && (_tableElement$dataset = tableElement.dataset) !== null && _tableElement$dataset !== void 0 && _tableElement$dataset.tableLocalId && tableElement.dataset.tableLocalId !== (tableNode === null || tableNode === void 0 ? void 0 : tableNode.attrs.localId)) { return; } if (!maybeTableCell || !tableRef) { return; } var htmlColIndex = maybeTableCell.cellIndex; // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting var rowElement = closestElement(target, 'tr'); var htmlRowIndex = rowElement && rowElement.rowIndex; var tableMap = tableNode && TableMap.get(tableNode); var colIndex = htmlColIndex; if (tableMap) { var convertedColIndex = convertHTMLCellIndexToColumnIndex(htmlColIndex, htmlRowIndex, tableMap); colIndex = getColumnIndexMappedToColumnIndexInFirstRow(convertedColIndex, htmlRowIndex, tableMap); } hoverCell(htmlRowIndex, colIndex)(view.state, view.dispatch); }; export var withCellTracking = function withCellTracking(eventHandler) { return function (view, mouseEvent) { if (getPluginState(view.state).isDragAndDropEnabled && getDragDropPluginState(view.state) && !getDragDropPluginState(view.state).isDragging) { trackCellLocation(view, mouseEvent); } return eventHandler(view, mouseEvent); }; };