mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-07-30 04:23:11 +03:00
Lexical: Imported core lexical libs
Imported at 0.17.1, Modified to work in-app. Added & configured test dependancies. Tests need to be altered to avoid using non-included deps including react dependancies.
This commit is contained in:
373
resources/js/wysiwyg/lexical/table/LexicalTableSelection.ts
Normal file
373
resources/js/wysiwyg/lexical/table/LexicalTableSelection.ts
Normal file
@ -0,0 +1,373 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import {$findMatchingParent} from '@lexical/utils';
|
||||
import {
|
||||
$createPoint,
|
||||
$getNodeByKey,
|
||||
$isElementNode,
|
||||
$normalizeSelection__EXPERIMENTAL,
|
||||
BaseSelection,
|
||||
isCurrentlyReadOnlyMode,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
PointType,
|
||||
} from 'lexical';
|
||||
import invariant from 'lexical/shared/invariant';
|
||||
|
||||
import {$isTableCellNode, TableCellNode} from './LexicalTableCellNode';
|
||||
import {$isTableNode} from './LexicalTableNode';
|
||||
import {$isTableRowNode} from './LexicalTableRowNode';
|
||||
import {$computeTableMap, $getTableCellNodeRect} from './LexicalTableUtils';
|
||||
|
||||
export type TableSelectionShape = {
|
||||
fromX: number;
|
||||
fromY: number;
|
||||
toX: number;
|
||||
toY: number;
|
||||
};
|
||||
|
||||
export type TableMapValueType = {
|
||||
cell: TableCellNode;
|
||||
startRow: number;
|
||||
startColumn: number;
|
||||
};
|
||||
export type TableMapType = Array<Array<TableMapValueType>>;
|
||||
|
||||
export class TableSelection implements BaseSelection {
|
||||
tableKey: NodeKey;
|
||||
anchor: PointType;
|
||||
focus: PointType;
|
||||
_cachedNodes: Array<LexicalNode> | null;
|
||||
dirty: boolean;
|
||||
|
||||
constructor(tableKey: NodeKey, anchor: PointType, focus: PointType) {
|
||||
this.anchor = anchor;
|
||||
this.focus = focus;
|
||||
anchor._selection = this;
|
||||
focus._selection = this;
|
||||
this._cachedNodes = null;
|
||||
this.dirty = false;
|
||||
this.tableKey = tableKey;
|
||||
}
|
||||
|
||||
getStartEndPoints(): [PointType, PointType] {
|
||||
return [this.anchor, this.focus];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the Selection is "backwards", meaning the focus
|
||||
* logically precedes the anchor in the EditorState.
|
||||
* @returns true if the Selection is backwards, false otherwise.
|
||||
*/
|
||||
isBackward(): boolean {
|
||||
return this.focus.isBefore(this.anchor);
|
||||
}
|
||||
|
||||
getCachedNodes(): LexicalNode[] | null {
|
||||
return this._cachedNodes;
|
||||
}
|
||||
|
||||
setCachedNodes(nodes: LexicalNode[] | null): void {
|
||||
this._cachedNodes = nodes;
|
||||
}
|
||||
|
||||
is(selection: null | BaseSelection): boolean {
|
||||
if (!$isTableSelection(selection)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
this.tableKey === selection.tableKey &&
|
||||
this.anchor.is(selection.anchor) &&
|
||||
this.focus.is(selection.focus)
|
||||
);
|
||||
}
|
||||
|
||||
set(tableKey: NodeKey, anchorCellKey: NodeKey, focusCellKey: NodeKey): void {
|
||||
this.dirty = true;
|
||||
this.tableKey = tableKey;
|
||||
this.anchor.key = anchorCellKey;
|
||||
this.focus.key = focusCellKey;
|
||||
this._cachedNodes = null;
|
||||
}
|
||||
|
||||
clone(): TableSelection {
|
||||
return new TableSelection(this.tableKey, this.anchor, this.focus);
|
||||
}
|
||||
|
||||
isCollapsed(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
extract(): Array<LexicalNode> {
|
||||
return this.getNodes();
|
||||
}
|
||||
|
||||
insertRawText(text: string): void {
|
||||
// Do nothing?
|
||||
}
|
||||
|
||||
insertText(): void {
|
||||
// Do nothing?
|
||||
}
|
||||
|
||||
insertNodes(nodes: Array<LexicalNode>) {
|
||||
const focusNode = this.focus.getNode();
|
||||
invariant(
|
||||
$isElementNode(focusNode),
|
||||
'Expected TableSelection focus to be an ElementNode',
|
||||
);
|
||||
const selection = $normalizeSelection__EXPERIMENTAL(
|
||||
focusNode.select(0, focusNode.getChildrenSize()),
|
||||
);
|
||||
selection.insertNodes(nodes);
|
||||
}
|
||||
|
||||
// TODO Deprecate this method. It's confusing when used with colspan|rowspan
|
||||
getShape(): TableSelectionShape {
|
||||
const anchorCellNode = $getNodeByKey(this.anchor.key);
|
||||
invariant(
|
||||
$isTableCellNode(anchorCellNode),
|
||||
'Expected TableSelection anchor to be (or a child of) TableCellNode',
|
||||
);
|
||||
const anchorCellNodeRect = $getTableCellNodeRect(anchorCellNode);
|
||||
invariant(
|
||||
anchorCellNodeRect !== null,
|
||||
'getCellRect: expected to find AnchorNode',
|
||||
);
|
||||
|
||||
const focusCellNode = $getNodeByKey(this.focus.key);
|
||||
invariant(
|
||||
$isTableCellNode(focusCellNode),
|
||||
'Expected TableSelection focus to be (or a child of) TableCellNode',
|
||||
);
|
||||
const focusCellNodeRect = $getTableCellNodeRect(focusCellNode);
|
||||
invariant(
|
||||
focusCellNodeRect !== null,
|
||||
'getCellRect: expected to find focusCellNode',
|
||||
);
|
||||
|
||||
const startX = Math.min(
|
||||
anchorCellNodeRect.columnIndex,
|
||||
focusCellNodeRect.columnIndex,
|
||||
);
|
||||
const stopX = Math.max(
|
||||
anchorCellNodeRect.columnIndex,
|
||||
focusCellNodeRect.columnIndex,
|
||||
);
|
||||
|
||||
const startY = Math.min(
|
||||
anchorCellNodeRect.rowIndex,
|
||||
focusCellNodeRect.rowIndex,
|
||||
);
|
||||
const stopY = Math.max(
|
||||
anchorCellNodeRect.rowIndex,
|
||||
focusCellNodeRect.rowIndex,
|
||||
);
|
||||
|
||||
return {
|
||||
fromX: Math.min(startX, stopX),
|
||||
fromY: Math.min(startY, stopY),
|
||||
toX: Math.max(startX, stopX),
|
||||
toY: Math.max(startY, stopY),
|
||||
};
|
||||
}
|
||||
|
||||
getNodes(): Array<LexicalNode> {
|
||||
const cachedNodes = this._cachedNodes;
|
||||
if (cachedNodes !== null) {
|
||||
return cachedNodes;
|
||||
}
|
||||
|
||||
const anchorNode = this.anchor.getNode();
|
||||
const focusNode = this.focus.getNode();
|
||||
const anchorCell = $findMatchingParent(anchorNode, $isTableCellNode);
|
||||
// todo replace with triplet
|
||||
const focusCell = $findMatchingParent(focusNode, $isTableCellNode);
|
||||
invariant(
|
||||
$isTableCellNode(anchorCell),
|
||||
'Expected TableSelection anchor to be (or a child of) TableCellNode',
|
||||
);
|
||||
invariant(
|
||||
$isTableCellNode(focusCell),
|
||||
'Expected TableSelection focus to be (or a child of) TableCellNode',
|
||||
);
|
||||
const anchorRow = anchorCell.getParent();
|
||||
invariant(
|
||||
$isTableRowNode(anchorRow),
|
||||
'Expected anchorCell to have a parent TableRowNode',
|
||||
);
|
||||
const tableNode = anchorRow.getParent();
|
||||
invariant(
|
||||
$isTableNode(tableNode),
|
||||
'Expected tableNode to have a parent TableNode',
|
||||
);
|
||||
|
||||
const focusCellGrid = focusCell.getParents()[1];
|
||||
if (focusCellGrid !== tableNode) {
|
||||
if (!tableNode.isParentOf(focusCell)) {
|
||||
// focus is on higher Grid level than anchor
|
||||
const gridParent = tableNode.getParent();
|
||||
invariant(gridParent != null, 'Expected gridParent to have a parent');
|
||||
this.set(this.tableKey, gridParent.getKey(), focusCell.getKey());
|
||||
} else {
|
||||
// anchor is on higher Grid level than focus
|
||||
const focusCellParent = focusCellGrid.getParent();
|
||||
invariant(
|
||||
focusCellParent != null,
|
||||
'Expected focusCellParent to have a parent',
|
||||
);
|
||||
this.set(this.tableKey, focusCell.getKey(), focusCellParent.getKey());
|
||||
}
|
||||
return this.getNodes();
|
||||
}
|
||||
|
||||
// TODO Mapping the whole Grid every time not efficient. We need to compute the entire state only
|
||||
// once (on load) and iterate on it as updates occur. However, to do this we need to have the
|
||||
// ability to store a state. Killing TableSelection and moving the logic to the plugin would make
|
||||
// this possible.
|
||||
const [map, cellAMap, cellBMap] = $computeTableMap(
|
||||
tableNode,
|
||||
anchorCell,
|
||||
focusCell,
|
||||
);
|
||||
|
||||
let minColumn = Math.min(cellAMap.startColumn, cellBMap.startColumn);
|
||||
let minRow = Math.min(cellAMap.startRow, cellBMap.startRow);
|
||||
let maxColumn = Math.max(
|
||||
cellAMap.startColumn + cellAMap.cell.__colSpan - 1,
|
||||
cellBMap.startColumn + cellBMap.cell.__colSpan - 1,
|
||||
);
|
||||
let maxRow = Math.max(
|
||||
cellAMap.startRow + cellAMap.cell.__rowSpan - 1,
|
||||
cellBMap.startRow + cellBMap.cell.__rowSpan - 1,
|
||||
);
|
||||
let exploredMinColumn = minColumn;
|
||||
let exploredMinRow = minRow;
|
||||
let exploredMaxColumn = minColumn;
|
||||
let exploredMaxRow = minRow;
|
||||
function expandBoundary(mapValue: TableMapValueType): void {
|
||||
const {
|
||||
cell,
|
||||
startColumn: cellStartColumn,
|
||||
startRow: cellStartRow,
|
||||
} = mapValue;
|
||||
minColumn = Math.min(minColumn, cellStartColumn);
|
||||
minRow = Math.min(minRow, cellStartRow);
|
||||
maxColumn = Math.max(maxColumn, cellStartColumn + cell.__colSpan - 1);
|
||||
maxRow = Math.max(maxRow, cellStartRow + cell.__rowSpan - 1);
|
||||
}
|
||||
while (
|
||||
minColumn < exploredMinColumn ||
|
||||
minRow < exploredMinRow ||
|
||||
maxColumn > exploredMaxColumn ||
|
||||
maxRow > exploredMaxRow
|
||||
) {
|
||||
if (minColumn < exploredMinColumn) {
|
||||
// Expand on the left
|
||||
const rowDiff = exploredMaxRow - exploredMinRow;
|
||||
const previousColumn = exploredMinColumn - 1;
|
||||
for (let i = 0; i <= rowDiff; i++) {
|
||||
expandBoundary(map[exploredMinRow + i][previousColumn]);
|
||||
}
|
||||
exploredMinColumn = previousColumn;
|
||||
}
|
||||
if (minRow < exploredMinRow) {
|
||||
// Expand on top
|
||||
const columnDiff = exploredMaxColumn - exploredMinColumn;
|
||||
const previousRow = exploredMinRow - 1;
|
||||
for (let i = 0; i <= columnDiff; i++) {
|
||||
expandBoundary(map[previousRow][exploredMinColumn + i]);
|
||||
}
|
||||
exploredMinRow = previousRow;
|
||||
}
|
||||
if (maxColumn > exploredMaxColumn) {
|
||||
// Expand on the right
|
||||
const rowDiff = exploredMaxRow - exploredMinRow;
|
||||
const nextColumn = exploredMaxColumn + 1;
|
||||
for (let i = 0; i <= rowDiff; i++) {
|
||||
expandBoundary(map[exploredMinRow + i][nextColumn]);
|
||||
}
|
||||
exploredMaxColumn = nextColumn;
|
||||
}
|
||||
if (maxRow > exploredMaxRow) {
|
||||
// Expand on the bottom
|
||||
const columnDiff = exploredMaxColumn - exploredMinColumn;
|
||||
const nextRow = exploredMaxRow + 1;
|
||||
for (let i = 0; i <= columnDiff; i++) {
|
||||
expandBoundary(map[nextRow][exploredMinColumn + i]);
|
||||
}
|
||||
exploredMaxRow = nextRow;
|
||||
}
|
||||
}
|
||||
|
||||
const nodes: Array<LexicalNode> = [tableNode];
|
||||
let lastRow = null;
|
||||
for (let i = minRow; i <= maxRow; i++) {
|
||||
for (let j = minColumn; j <= maxColumn; j++) {
|
||||
const {cell} = map[i][j];
|
||||
const currentRow = cell.getParent();
|
||||
invariant(
|
||||
$isTableRowNode(currentRow),
|
||||
'Expected TableCellNode parent to be a TableRowNode',
|
||||
);
|
||||
if (currentRow !== lastRow) {
|
||||
nodes.push(currentRow);
|
||||
}
|
||||
nodes.push(cell, ...$getChildrenRecursively(cell));
|
||||
lastRow = currentRow;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCurrentlyReadOnlyMode()) {
|
||||
this._cachedNodes = nodes;
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
const nodes = this.getNodes().filter((node) => $isTableCellNode(node));
|
||||
let textContent = '';
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
const row = node.__parent;
|
||||
const nextRow = (nodes[i + 1] || {}).__parent;
|
||||
textContent += node.getTextContent() + (nextRow !== row ? '\n' : '\t');
|
||||
}
|
||||
return textContent;
|
||||
}
|
||||
}
|
||||
|
||||
export function $isTableSelection(x: unknown): x is TableSelection {
|
||||
return x instanceof TableSelection;
|
||||
}
|
||||
|
||||
export function $createTableSelection(): TableSelection {
|
||||
const anchor = $createPoint('root', 0, 'element');
|
||||
const focus = $createPoint('root', 0, 'element');
|
||||
return new TableSelection('root', anchor, focus);
|
||||
}
|
||||
|
||||
export function $getChildrenRecursively(node: LexicalNode): Array<LexicalNode> {
|
||||
const nodes = [];
|
||||
const stack = [node];
|
||||
while (stack.length > 0) {
|
||||
const currentNode = stack.pop();
|
||||
invariant(
|
||||
currentNode !== undefined,
|
||||
"Stack.length > 0; can't be undefined",
|
||||
);
|
||||
if ($isElementNode(currentNode)) {
|
||||
stack.unshift(...currentNode.getChildren());
|
||||
}
|
||||
if (currentNode !== node) {
|
||||
nodes.push(currentNode);
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
Reference in New Issue
Block a user