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