@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
151 lines (145 loc) • 6.63 kB
JavaScript
import { INPUT_METHOD } from '@atlaskit/editor-common/analytics';
import { isSelectionTableNestedInTable } from '@atlaskit/editor-common/nesting';
import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
import { findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
import { TableMap } from '@atlaskit/editor-tables/table-map';
import { goToNextCell as baseGotoNextCell, findCellClosestToPos, findTable, findTableClosestToPos, isTableSelected } from '@atlaskit/editor-tables/utils';
import { getPluginState } from '../plugin-factory';
import { stopKeyboardColumnResizing } from './column-resize';
import { insertRowWithAnalytics } from './commands-with-analytics';
var TAB_FORWARD_DIRECTION = 1;
var TAB_BACKWARD_DIRECTION = -1;
export var goToNextCell = function goToNextCell(editorAnalyticsAPI, ariaNotify, getIntl) {
return function (direction) {
return function (state, dispatch, view) {
var _getPluginState;
var table = findTable(state.selection);
if (!table) {
return false;
}
var isColumnResizing = (_getPluginState = getPluginState(state)) === null || _getPluginState === void 0 ? void 0 : _getPluginState.isKeyboardResize;
if (isColumnResizing) {
stopKeyboardColumnResizing({
ariaNotify: ariaNotify,
getIntl: getIntl
})(state, dispatch, view);
return true;
}
var map = TableMap.get(table.node);
var _state$schema$nodes = state.schema.nodes,
tableCell = _state$schema$nodes.tableCell,
tableHeader = _state$schema$nodes.tableHeader;
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
var cell = findParentNodeOfType([tableCell, tableHeader])(state.selection);
var firstCellPos = map.positionAt(0, 0, table.node) + table.start;
var lastCellPos = map.positionAt(map.height - 1, map.width - 1, table.node) + table.start;
// When tabbing backwards at first cell (top left), insert row at the start of table
if (firstCellPos === cell.pos && direction === TAB_BACKWARD_DIRECTION) {
insertRowWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.KEYBOARD, {
index: 0,
moveCursorToInsertedRow: true
})(state, dispatch);
return true;
}
// When tabbing forwards at last cell (bottom right), insert row at the end of table
if (lastCellPos === cell.pos && direction === TAB_FORWARD_DIRECTION) {
insertRowWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.KEYBOARD, {
index: map.height,
moveCursorToInsertedRow: true
})(state, dispatch);
return true;
}
if (dispatch) {
return baseGotoNextCell(direction)(state, dispatch);
}
return true;
};
};
};
/**
* Moves the cursor vertically from a NodeSelection within a table cell.
* - If content exists above/below within the cell, lets ProseMirror handle it.
* - Otherwise, moves to the next cell (down to start, up to last line).
*/
export var goToNextCellVertical = function goToNextCellVertical(direction) {
return function (state, dispatch) {
var selection = state.selection;
var nestedTableSelection = isSelectionTableNestedInTable(state);
if (!(selection instanceof NodeSelection) && !nestedTableSelection) {
return false; // Let ProseMirror handle other selection types
}
var table = findTable(selection);
var cell = findCellClosestToPos(state.doc.resolve(selection.from));
var selectionPos = selection.from;
// Handle when we have nested table fully selected
if (table && nestedTableSelection && isTableSelected(selection)) {
var parentTablePos = table.pos;
table = findTableClosestToPos(state.doc.resolve(parentTablePos));
cell = findCellClosestToPos(state.doc.resolve(parentTablePos));
selectionPos = parentTablePos;
}
if (!table || !cell || !cell.pos) {
return false;
}
var _state$schema$nodes2 = state.schema.nodes,
tableCell = _state$schema$nodes2.tableCell,
tableHeader = _state$schema$nodes2.tableHeader;
// Let ProseMirror handle movement within the cell if content exists above/below
if (cellHasContentInDirection(cell.node, cell.pos, selectionPos, direction)) {
return false;
}
// Move to the next cell vertically
var map = TableMap.get(table.node);
var nextCellPos = map.nextCell(cell.pos - table.start, 'vert', direction);
if (dispatch && nextCellPos) {
var _$nextCell$nodeAfter, _$nextCell$nodeAfter2;
var nextCellStart = table.start + nextCellPos;
var $nextCell = state.doc.resolve(nextCellStart);
if ((_$nextCell$nodeAfter = $nextCell.nodeAfter) !== null && _$nextCell$nodeAfter !== void 0 && _$nextCell$nodeAfter.type && [tableCell, tableHeader].includes((_$nextCell$nodeAfter2 = $nextCell.nodeAfter) === null || _$nextCell$nodeAfter2 === void 0 ? void 0 : _$nextCell$nodeAfter2.type)) {
var contentPos = getTargetPositionInNextCell($nextCell.nodeAfter, nextCellStart, direction);
dispatch(state.tr.setSelection(TextSelection.create(state.doc, contentPos)));
return true;
}
return false;
}
return false; // No next cell found
};
};
function cellHasContentInDirection(cellNode, cellPos, selectionPos, direction) {
var hasContent = false;
cellNode.content.forEach(function (node, offset) {
var nodeStart = cellPos + 1 + offset;
var nodeEnd = nodeStart + node.nodeSize;
if (direction === 1 && nodeStart > selectionPos && node.isBlock) {
hasContent = true; // Content below
} else if (direction === -1 && nodeEnd <= selectionPos && node.isBlock) {
hasContent = true; // Content above
}
});
return hasContent;
}
function getTargetPositionInNextCell(cellNode, nextCellStart, direction) {
var contentPos = nextCellStart + 1; // Default: just inside the cell
if (cellNode.content.size > 0) {
if (direction === 1) {
var firstChild = cellNode.firstChild;
if (firstChild && firstChild.isBlock) {
contentPos = nextCellStart + 1 + (firstChild.isLeaf ? 0 : 1); // Down: Start of first block
}
} else if (direction === -1) {
var lastBlock;
var lastOffset = 0;
cellNode.content.forEach(function (node, offset) {
if (node.isBlock) {
lastBlock = node;
lastOffset = offset;
}
});
if (lastBlock) {
contentPos = nextCellStart + 1 + lastOffset + (lastBlock.isLeaf ? 0 : lastBlock.nodeSize - 1);
}
}
}
return contentPos;
}