UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

421 lines (418 loc) 17.1 kB
import { tableMessages as messages } from '@atlaskit/editor-common/messages'; import { GapCursorSelection, isSelectionAtEndOfNode, isSelectionAtStartOfNode, RelativeSelectionPos, Side } from '@atlaskit/editor-common/selection'; import { Selection, TextSelection } from '@atlaskit/editor-prosemirror/state'; import { CellSelection } from '@atlaskit/editor-tables/cell-selection'; import { TableMap } from '@atlaskit/editor-tables/table-map'; import { findTable, isColumnSelected, isRowSelected, isTableSelected, selectedRect } from '@atlaskit/editor-tables/utils'; import { getClosestSelectionRect } from '../../ui/toolbar'; import { getPluginState } from '../plugin-factory'; import { selectColumn, selectRow } from './misc'; var TableSelectionDirection = /*#__PURE__*/function (TableSelectionDirection) { TableSelectionDirection["TopToBottom"] = "TopToBottom"; TableSelectionDirection["BottomToTop"] = "BottomToTop"; return TableSelectionDirection; }(TableSelectionDirection || {}); export const arrowLeftFromTable = editorSelectionAPI => () => (state, dispatch) => { const { selection } = state; if (selection instanceof CellSelection) { return arrowLeftFromCellSelection(editorSelectionAPI)(selection)(state, dispatch); } else if (selection instanceof GapCursorSelection) { return arrowLeftFromGapCursor(editorSelectionAPI)(selection)(state, dispatch); } else if (selection instanceof TextSelection) { return arrowLeftFromText(editorSelectionAPI)(selection)(state, dispatch); } return false; }; export const arrowRightFromTable = editorSelectionAPI => () => (state, dispatch) => { const { selection } = state; if (selection instanceof CellSelection) { return arrowRightFromCellSelection(editorSelectionAPI)(selection)(state, dispatch); } else if (selection instanceof GapCursorSelection) { return arrowRightFromGapCursor(editorSelectionAPI)(selection)(state, dispatch); } else if (selection instanceof TextSelection) { return arrowRightFromText(editorSelectionAPI)(selection)(state, dispatch); } return false; }; const arrowLeftFromCellSelection = editorSelectionAPI => selection => (state, dispatch) => { if (isTableSelected(state.selection) && editorSelectionAPI) { const selectionState = editorSelectionAPI.sharedState.currentState() || {}; if ((selectionState === null || selectionState === void 0 ? void 0 : selectionState.selectionRelativeToNode) === RelativeSelectionPos.Start) { // we have full table cell selection and want to set gap cursor selection before table return setGapCursorBeforeTable(editorSelectionAPI)()(state, dispatch); } else if ((selectionState === null || selectionState === void 0 ? void 0 : selectionState.selectionRelativeToNode) === RelativeSelectionPos.End) { // we have full table cell selection and want to set selection at end of last cell return setSelectionAtEndOfLastCell(editorSelectionAPI)(selection)(state, dispatch); } else if ((selectionState === null || selectionState === void 0 ? void 0 : selectionState.selectionRelativeToNode) === undefined) { // we have full table cell selection and want to set selection at start of first cell return setSelectionAtStartOfFirstCell(editorSelectionAPI)(selection, RelativeSelectionPos.Before)(state, dispatch); } } return false; }; const arrowRightFromCellSelection = editorSelectionAPI => selection => (state, dispatch) => { if (isTableSelected(state.selection) && editorSelectionAPI) { const { selectionRelativeToNode } = editorSelectionAPI.sharedState.currentState() || {}; if (selectionRelativeToNode === RelativeSelectionPos.Start) { // we have full table cell selection and want to set selection at start of first cell return setSelectionAtStartOfFirstCell(editorSelectionAPI)(selection)(state, dispatch); } else if (selectionRelativeToNode === RelativeSelectionPos.End || selectionRelativeToNode === undefined) { // we have full table cell selection and want to set gap cursor selection after table return setGapCursorAfterTable(editorSelectionAPI)()(state, dispatch); } } return false; }; export const selectColumns = (editorSelectionAPI, ariaNotify, getIntl) => (triggeredByKeyboard = false) => (state, dispatch) => { const { selection } = state; const table = findTable(selection); const rect = selectedRect(state); if (table && isRowSelected(rect.top)(selection)) { return selectFullTable(editorSelectionAPI)({ node: table.node, startPos: table.start, dir: TableSelectionDirection.BottomToTop })(state, dispatch); } if (table && rect) { const selectColumnCommand = selectColumn(rect.left, undefined, triggeredByKeyboard)(state, dispatch); const map = TableMap.get(table.node); if (ariaNotify && getIntl) { ariaNotify(getIntl().formatMessage(messages.columnSelected, { index: rect.left + 1, total: map.width }), { priority: 'important' }); } return selectColumnCommand; } return false; }; export const selectRows = (editorSelectionAPI, ariaNotify, getIntl) => (triggeredByKeyboard = false) => (state, dispatch) => { const { selection } = state; const table = findTable(selection); const rect = selectedRect(state); if (table && isColumnSelected(rect.left)(selection)) { return selectFullTable(editorSelectionAPI)({ node: table.node, startPos: table.start, dir: TableSelectionDirection.BottomToTop })(state, dispatch); } if (table && rect) { const selectRowCommand = selectRow(rect.top, undefined, triggeredByKeyboard)(state, dispatch); const map = TableMap.get(table.node); if (ariaNotify && getIntl) { ariaNotify(getIntl().formatMessage(messages.rowSelected, { index: rect.top + 1, total: map.height }), { priority: 'important' }); } return selectRowCommand; } return false; }; const arrowLeftFromGapCursor = editorSelectionAPI => selection => (state, dispatch) => { const { doc } = state; const { $from, from, side } = selection; if (side === Side.RIGHT) { if ($from.nodeBefore && $from.nodeBefore.type.name === 'table') { // we have a gap cursor after a table node and want to set a full table cell selection return selectFullTable(editorSelectionAPI)({ node: $from.nodeBefore, startPos: doc.resolve(from - 1).start($from.depth + 1), dir: TableSelectionDirection.TopToBottom })(state, dispatch); } } else if (side === Side.LEFT) { const table = findTable(selection); if (table && isSelectionAtStartOfTable($from, selection) && editorSelectionAPI) { const selectionState = editorSelectionAPI.sharedState.currentState() || {}; if ((selectionState === null || selectionState === void 0 ? void 0 : selectionState.selectionRelativeToNode) === RelativeSelectionPos.Before) { // we have a gap cursor at start of first table cell and want to set a gap cursor selection before table return setGapCursorBeforeTable(editorSelectionAPI)()(state, dispatch); } else { // we have a gap cursor at start of first table cell and want to set a full table cell selection return selectFullTable(editorSelectionAPI)({ node: table.node, startPos: table.start, dir: TableSelectionDirection.BottomToTop })(state, dispatch); } } } return false; }; const arrowRightFromGapCursor = editorSelectionAPI => selection => (state, dispatch) => { const { $from, from, $to, side } = selection; if (side === Side.LEFT) { if ($from.nodeAfter && $from.nodeAfter.type.name === 'table') { // we have a gap cursor before a table node and want to set a full table cell selection return selectFullTable(editorSelectionAPI)({ node: $from.nodeAfter, startPos: from + 1, dir: TableSelectionDirection.BottomToTop })(state, dispatch); } } else if (side === Side.RIGHT) { const table = findTable(selection); if (table && isSelectionAtEndOfTable($to, selection)) { // we have a gap cursor at end of last table cell and want to set a full table cell selection return selectFullTable(editorSelectionAPI)({ node: table.node, startPos: table.start, dir: TableSelectionDirection.TopToBottom })(state, dispatch); } } return false; }; const arrowLeftFromText = editorSelectionAPI => selection => (state, dispatch) => { const table = findTable(selection); if (table) { const { $from } = selection; const columResizePluginState = getPluginState(state) || {}; const isColumnResizing = Boolean(columResizePluginState === null || columResizePluginState === void 0 ? void 0 : columResizePluginState.isKeyboardResize); if (isSelectionAtStartOfTable($from, selection) && $from.parent.type.name === 'paragraph' && $from.depth === table.depth + 3 && // + 3 for: row, cell & paragraph nodes editorSelectionAPI && !isColumnResizing) { const selectionState = editorSelectionAPI.sharedState.currentState() || {}; if ((selectionState === null || selectionState === void 0 ? void 0 : selectionState.selectionRelativeToNode) === RelativeSelectionPos.Before) { // we have a text selection at start of first table cell, directly inside a top level paragraph, // and want to set gap cursor selection before table return setGapCursorBeforeTable(editorSelectionAPI)()(state, dispatch); } else { // we have a text selection at start of first table cell, directly inside a top level paragraph, // and want to set a full table cell selection return selectFullTable(editorSelectionAPI)({ node: table.node, startPos: table.start, dir: TableSelectionDirection.BottomToTop })(state, dispatch); } } } return false; }; const arrowRightFromText = editorSelectionAPI => selection => (state, dispatch) => { const table = findTable(selection); if (table) { const { $to } = selection; const columResizePluginState = getPluginState(state) || {}; const isColumnResizing = Boolean(columResizePluginState === null || columResizePluginState === void 0 ? void 0 : columResizePluginState.isKeyboardResize); if (isSelectionAtEndOfTable($to, selection) && $to.parent.type.name === 'paragraph' && $to.depth === table.depth + 3 && // + 3 for: row, cell & paragraph nodes !isColumnResizing) { // we have a text selection at end of last table cell, directly inside a top level paragraph, // and want to set a full table cell selection return selectFullTable(editorSelectionAPI)({ node: table.node, startPos: table.start, dir: TableSelectionDirection.TopToBottom })(state, dispatch); } } return false; }; /** * Sets a cell selection over all the cells in the table node * We use this instead of selectTable from prosemirror-utils so we can control which * cell is the anchor and which is the head, and also so we can set the relative selection * pos in the selection plugin */ const selectFullTable = editorSelectionAPI => ({ node, startPos, dir }) => (state, dispatch) => { const { doc } = state; const { map } = TableMap.get(node); const $firstCell = doc.resolve(startPos + map[0]); const $lastCell = doc.resolve(startPos + map[map.length - 1]); let fullTableSelection; let selectionRelativeToNode; if (dir === TableSelectionDirection.TopToBottom) { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any fullTableSelection = new CellSelection($firstCell, $lastCell); selectionRelativeToNode = RelativeSelectionPos.End; } else { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any fullTableSelection = new CellSelection($lastCell, $firstCell); selectionRelativeToNode = RelativeSelectionPos.Start; } if (editorSelectionAPI) { const tr = editorSelectionAPI.actions.selectNearNode({ selectionRelativeToNode, selection: fullTableSelection })(state); if (dispatch) { dispatch(tr); return true; } } return false; }; const setSelectionAtStartOfFirstCell = editorSelectionAPI => (selection, selectionRelativeToNode) => (state, dispatch) => { const { $anchorCell, $headCell } = selection; const $firstCell = $anchorCell.pos < $headCell.pos ? $anchorCell : $headCell; const $firstPosInsideCell = state.doc.resolve($firstCell.pos + 1); // check if first pos should have a gap cursor, otherwise find closest text selection const selectionAtStartOfCell = GapCursorSelection.valid($firstPosInsideCell) ? new GapCursorSelection($firstPosInsideCell, Side.LEFT) : Selection.findFrom($firstPosInsideCell, 1); if (editorSelectionAPI) { const tr = editorSelectionAPI.actions.selectNearNode({ selectionRelativeToNode, selection: selectionAtStartOfCell })(state); if (dispatch) { dispatch(tr); return true; } } return false; }; const setSelectionAtEndOfLastCell = editorSelectionAPI => (selection, selectionRelativeToNode) => (state, dispatch) => { const { $anchorCell, $headCell } = selection; const $lastCell = $anchorCell.pos > $headCell.pos ? $anchorCell : $headCell; const lastPosInsideCell = $lastCell.pos + ($lastCell.nodeAfter ? $lastCell.nodeAfter.content.size : 0) + 1; const $lastPosInsideCell = state.doc.resolve(lastPosInsideCell); // check if last pos should have a gap cursor, otherwise find closest text selection const selectionAtEndOfCell = GapCursorSelection.valid($lastPosInsideCell) ? new GapCursorSelection($lastPosInsideCell, Side.RIGHT) : Selection.findFrom($lastPosInsideCell, -1); if (editorSelectionAPI) { const tr = editorSelectionAPI.actions.selectNearNode({ selectionRelativeToNode, selection: selectionAtEndOfCell })(state); if (dispatch) { dispatch(tr); return true; } } return false; }; const setGapCursorBeforeTable = editorSelectionAPI => () => (state, dispatch) => { const table = findTable(state.selection); if (table) { const $beforeTablePos = state.doc.resolve(table.pos); if (GapCursorSelection.valid($beforeTablePos)) { const selectionBeforeTable = new GapCursorSelection($beforeTablePos, Side.LEFT); if (editorSelectionAPI) { const tr = editorSelectionAPI.actions.selectNearNode({ selectionRelativeToNode: undefined, selection: selectionBeforeTable })(state); if (dispatch) { dispatch(tr); return true; } } } } return false; }; const setGapCursorAfterTable = editorSelectionAPI => () => (state, dispatch) => { const table = findTable(state.selection); if (table) { const $afterTablePos = state.doc.resolve(table.pos + table.node.nodeSize); if (GapCursorSelection.valid($afterTablePos)) { const selectionAfterTable = new GapCursorSelection($afterTablePos, Side.RIGHT); if (editorSelectionAPI) { const tr = editorSelectionAPI.actions.selectNearNode({ selectionRelativeToNode: undefined, selection: selectionAfterTable })(state); if (dispatch) { dispatch(tr); return true; } } return false; } } return false; }; const isSelectionAtStartOfTable = ($pos, selection) => { var _findTable; return isSelectionAtStartOfNode($pos, (_findTable = findTable(selection)) === null || _findTable === void 0 ? void 0 : _findTable.node); }; const isSelectionAtEndOfTable = ($pos, selection) => { var _findTable2; return isSelectionAtEndOfNode($pos, (_findTable2 = findTable(selection)) === null || _findTable2 === void 0 ? void 0 : _findTable2.node); }; export const shiftArrowUpFromTable = editorSelectionAPI => () => (state, dispatch) => { const { selection } = state; const table = findTable(selection); const selectionRect = getClosestSelectionRect(state); const index = selectionRect === null || selectionRect === void 0 ? void 0 : selectionRect.top; if (table && index === 0) { return selectFullTable(editorSelectionAPI)({ node: table.node, startPos: table.start, dir: TableSelectionDirection.BottomToTop })(state, dispatch); } return false; }; export const modASelectTable = editorSelectionAPI => () => (state, dispatch) => { const { selection } = state; const table = findTable(selection); if (!table) { return false; } const { $from, $to } = selection; const tableSelected = isTableSelected(selection); if (!tableSelected && $from.pos > table.start + 1 && $to.pos < table.start + table.node.nodeSize) { return selectFullTable(editorSelectionAPI)({ node: table.node, startPos: table.start, dir: TableSelectionDirection.BottomToTop })(state, dispatch); } return false; };