mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-07-31 15:24:31 +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:
414
resources/js/wysiwyg/lexical/table/LexicalTableObserver.ts
Normal file
414
resources/js/wysiwyg/lexical/table/LexicalTableObserver.ts
Normal file
@ -0,0 +1,414 @@
|
||||
/**
|
||||
* 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 type {LexicalEditor, NodeKey, TextFormatType} from 'lexical';
|
||||
|
||||
import {
|
||||
addClassNamesToElement,
|
||||
removeClassNamesFromElement,
|
||||
} from '@lexical/utils';
|
||||
import {
|
||||
$createParagraphNode,
|
||||
$createRangeSelection,
|
||||
$createTextNode,
|
||||
$getNearestNodeFromDOMNode,
|
||||
$getNodeByKey,
|
||||
$getRoot,
|
||||
$getSelection,
|
||||
$isElementNode,
|
||||
$setSelection,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
} from 'lexical';
|
||||
import invariant from 'lexical/shared/invariant';
|
||||
|
||||
import {$isTableCellNode} from './LexicalTableCellNode';
|
||||
import {$isTableNode} from './LexicalTableNode';
|
||||
import {
|
||||
$createTableSelection,
|
||||
$isTableSelection,
|
||||
type TableSelection,
|
||||
} from './LexicalTableSelection';
|
||||
import {
|
||||
$findTableNode,
|
||||
$updateDOMForSelection,
|
||||
getDOMSelection,
|
||||
getTable,
|
||||
} from './LexicalTableSelectionHelpers';
|
||||
|
||||
export type TableDOMCell = {
|
||||
elem: HTMLElement;
|
||||
highlighted: boolean;
|
||||
hasBackgroundColor: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type TableDOMRows = Array<Array<TableDOMCell | undefined> | undefined>;
|
||||
|
||||
export type TableDOMTable = {
|
||||
domRows: TableDOMRows;
|
||||
columns: number;
|
||||
rows: number;
|
||||
};
|
||||
|
||||
export class TableObserver {
|
||||
focusX: number;
|
||||
focusY: number;
|
||||
listenersToRemove: Set<() => void>;
|
||||
table: TableDOMTable;
|
||||
isHighlightingCells: boolean;
|
||||
anchorX: number;
|
||||
anchorY: number;
|
||||
tableNodeKey: NodeKey;
|
||||
anchorCell: TableDOMCell | null;
|
||||
focusCell: TableDOMCell | null;
|
||||
anchorCellNodeKey: NodeKey | null;
|
||||
focusCellNodeKey: NodeKey | null;
|
||||
editor: LexicalEditor;
|
||||
tableSelection: TableSelection | null;
|
||||
hasHijackedSelectionStyles: boolean;
|
||||
isSelecting: boolean;
|
||||
|
||||
constructor(editor: LexicalEditor, tableNodeKey: string) {
|
||||
this.isHighlightingCells = false;
|
||||
this.anchorX = -1;
|
||||
this.anchorY = -1;
|
||||
this.focusX = -1;
|
||||
this.focusY = -1;
|
||||
this.listenersToRemove = new Set();
|
||||
this.tableNodeKey = tableNodeKey;
|
||||
this.editor = editor;
|
||||
this.table = {
|
||||
columns: 0,
|
||||
domRows: [],
|
||||
rows: 0,
|
||||
};
|
||||
this.tableSelection = null;
|
||||
this.anchorCellNodeKey = null;
|
||||
this.focusCellNodeKey = null;
|
||||
this.anchorCell = null;
|
||||
this.focusCell = null;
|
||||
this.hasHijackedSelectionStyles = false;
|
||||
this.trackTable();
|
||||
this.isSelecting = false;
|
||||
}
|
||||
|
||||
getTable(): TableDOMTable {
|
||||
return this.table;
|
||||
}
|
||||
|
||||
removeListeners() {
|
||||
Array.from(this.listenersToRemove).forEach((removeListener) =>
|
||||
removeListener(),
|
||||
);
|
||||
}
|
||||
|
||||
trackTable() {
|
||||
const observer = new MutationObserver((records) => {
|
||||
this.editor.update(() => {
|
||||
let gridNeedsRedraw = false;
|
||||
|
||||
for (let i = 0; i < records.length; i++) {
|
||||
const record = records[i];
|
||||
const target = record.target;
|
||||
const nodeName = target.nodeName;
|
||||
|
||||
if (
|
||||
nodeName === 'TABLE' ||
|
||||
nodeName === 'TBODY' ||
|
||||
nodeName === 'THEAD' ||
|
||||
nodeName === 'TR'
|
||||
) {
|
||||
gridNeedsRedraw = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!gridNeedsRedraw) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tableElement = this.editor.getElementByKey(this.tableNodeKey);
|
||||
|
||||
if (!tableElement) {
|
||||
throw new Error('Expected to find TableElement in DOM');
|
||||
}
|
||||
|
||||
this.table = getTable(tableElement);
|
||||
});
|
||||
});
|
||||
this.editor.update(() => {
|
||||
const tableElement = this.editor.getElementByKey(this.tableNodeKey);
|
||||
|
||||
if (!tableElement) {
|
||||
throw new Error('Expected to find TableElement in DOM');
|
||||
}
|
||||
|
||||
this.table = getTable(tableElement);
|
||||
observer.observe(tableElement, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
clearHighlight() {
|
||||
const editor = this.editor;
|
||||
this.isHighlightingCells = false;
|
||||
this.anchorX = -1;
|
||||
this.anchorY = -1;
|
||||
this.focusX = -1;
|
||||
this.focusY = -1;
|
||||
this.tableSelection = null;
|
||||
this.anchorCellNodeKey = null;
|
||||
this.focusCellNodeKey = null;
|
||||
this.anchorCell = null;
|
||||
this.focusCell = null;
|
||||
this.hasHijackedSelectionStyles = false;
|
||||
|
||||
this.enableHighlightStyle();
|
||||
|
||||
editor.update(() => {
|
||||
const tableNode = $getNodeByKey(this.tableNodeKey);
|
||||
|
||||
if (!$isTableNode(tableNode)) {
|
||||
throw new Error('Expected TableNode.');
|
||||
}
|
||||
|
||||
const tableElement = editor.getElementByKey(this.tableNodeKey);
|
||||
|
||||
if (!tableElement) {
|
||||
throw new Error('Expected to find TableElement in DOM');
|
||||
}
|
||||
|
||||
const grid = getTable(tableElement);
|
||||
$updateDOMForSelection(editor, grid, null);
|
||||
$setSelection(null);
|
||||
editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
|
||||
});
|
||||
}
|
||||
|
||||
enableHighlightStyle() {
|
||||
const editor = this.editor;
|
||||
editor.update(() => {
|
||||
const tableElement = editor.getElementByKey(this.tableNodeKey);
|
||||
|
||||
if (!tableElement) {
|
||||
throw new Error('Expected to find TableElement in DOM');
|
||||
}
|
||||
|
||||
removeClassNamesFromElement(
|
||||
tableElement,
|
||||
editor._config.theme.tableSelection,
|
||||
);
|
||||
tableElement.classList.remove('disable-selection');
|
||||
this.hasHijackedSelectionStyles = false;
|
||||
});
|
||||
}
|
||||
|
||||
disableHighlightStyle() {
|
||||
const editor = this.editor;
|
||||
editor.update(() => {
|
||||
const tableElement = editor.getElementByKey(this.tableNodeKey);
|
||||
|
||||
if (!tableElement) {
|
||||
throw new Error('Expected to find TableElement in DOM');
|
||||
}
|
||||
|
||||
addClassNamesToElement(tableElement, editor._config.theme.tableSelection);
|
||||
this.hasHijackedSelectionStyles = true;
|
||||
});
|
||||
}
|
||||
|
||||
updateTableTableSelection(selection: TableSelection | null): void {
|
||||
if (selection !== null && selection.tableKey === this.tableNodeKey) {
|
||||
const editor = this.editor;
|
||||
this.tableSelection = selection;
|
||||
this.isHighlightingCells = true;
|
||||
this.disableHighlightStyle();
|
||||
$updateDOMForSelection(editor, this.table, this.tableSelection);
|
||||
} else if (selection == null) {
|
||||
this.clearHighlight();
|
||||
} else {
|
||||
this.tableNodeKey = selection.tableKey;
|
||||
this.updateTableTableSelection(selection);
|
||||
}
|
||||
}
|
||||
|
||||
setFocusCellForSelection(cell: TableDOMCell, ignoreStart = false) {
|
||||
const editor = this.editor;
|
||||
editor.update(() => {
|
||||
const tableNode = $getNodeByKey(this.tableNodeKey);
|
||||
|
||||
if (!$isTableNode(tableNode)) {
|
||||
throw new Error('Expected TableNode.');
|
||||
}
|
||||
|
||||
const tableElement = editor.getElementByKey(this.tableNodeKey);
|
||||
|
||||
if (!tableElement) {
|
||||
throw new Error('Expected to find TableElement in DOM');
|
||||
}
|
||||
|
||||
const cellX = cell.x;
|
||||
const cellY = cell.y;
|
||||
this.focusCell = cell;
|
||||
|
||||
if (this.anchorCell !== null) {
|
||||
const domSelection = getDOMSelection(editor._window);
|
||||
// Collapse the selection
|
||||
if (domSelection) {
|
||||
domSelection.setBaseAndExtent(
|
||||
this.anchorCell.elem,
|
||||
0,
|
||||
this.focusCell.elem,
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!this.isHighlightingCells &&
|
||||
(this.anchorX !== cellX || this.anchorY !== cellY || ignoreStart)
|
||||
) {
|
||||
this.isHighlightingCells = true;
|
||||
this.disableHighlightStyle();
|
||||
} else if (cellX === this.focusX && cellY === this.focusY) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.focusX = cellX;
|
||||
this.focusY = cellY;
|
||||
|
||||
if (this.isHighlightingCells) {
|
||||
const focusTableCellNode = $getNearestNodeFromDOMNode(cell.elem);
|
||||
|
||||
if (
|
||||
this.tableSelection != null &&
|
||||
this.anchorCellNodeKey != null &&
|
||||
$isTableCellNode(focusTableCellNode) &&
|
||||
tableNode.is($findTableNode(focusTableCellNode))
|
||||
) {
|
||||
const focusNodeKey = focusTableCellNode.getKey();
|
||||
|
||||
this.tableSelection =
|
||||
this.tableSelection.clone() || $createTableSelection();
|
||||
|
||||
this.focusCellNodeKey = focusNodeKey;
|
||||
this.tableSelection.set(
|
||||
this.tableNodeKey,
|
||||
this.anchorCellNodeKey,
|
||||
this.focusCellNodeKey,
|
||||
);
|
||||
|
||||
$setSelection(this.tableSelection);
|
||||
|
||||
editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
|
||||
|
||||
$updateDOMForSelection(editor, this.table, this.tableSelection);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setAnchorCellForSelection(cell: TableDOMCell) {
|
||||
this.isHighlightingCells = false;
|
||||
this.anchorCell = cell;
|
||||
this.anchorX = cell.x;
|
||||
this.anchorY = cell.y;
|
||||
|
||||
this.editor.update(() => {
|
||||
const anchorTableCellNode = $getNearestNodeFromDOMNode(cell.elem);
|
||||
|
||||
if ($isTableCellNode(anchorTableCellNode)) {
|
||||
const anchorNodeKey = anchorTableCellNode.getKey();
|
||||
this.tableSelection =
|
||||
this.tableSelection != null
|
||||
? this.tableSelection.clone()
|
||||
: $createTableSelection();
|
||||
this.anchorCellNodeKey = anchorNodeKey;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
formatCells(type: TextFormatType) {
|
||||
this.editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
|
||||
if (!$isTableSelection(selection)) {
|
||||
invariant(false, 'Expected grid selection');
|
||||
}
|
||||
|
||||
const formatSelection = $createRangeSelection();
|
||||
|
||||
const anchor = formatSelection.anchor;
|
||||
const focus = formatSelection.focus;
|
||||
|
||||
selection.getNodes().forEach((cellNode) => {
|
||||
if ($isTableCellNode(cellNode) && cellNode.getTextContentSize() !== 0) {
|
||||
anchor.set(cellNode.getKey(), 0, 'element');
|
||||
focus.set(cellNode.getKey(), cellNode.getChildrenSize(), 'element');
|
||||
formatSelection.formatText(type);
|
||||
}
|
||||
});
|
||||
|
||||
$setSelection(selection);
|
||||
|
||||
this.editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
|
||||
});
|
||||
}
|
||||
|
||||
clearText() {
|
||||
const editor = this.editor;
|
||||
editor.update(() => {
|
||||
const tableNode = $getNodeByKey(this.tableNodeKey);
|
||||
|
||||
if (!$isTableNode(tableNode)) {
|
||||
throw new Error('Expected TableNode.');
|
||||
}
|
||||
|
||||
const selection = $getSelection();
|
||||
|
||||
if (!$isTableSelection(selection)) {
|
||||
invariant(false, 'Expected grid selection');
|
||||
}
|
||||
|
||||
const selectedNodes = selection.getNodes().filter($isTableCellNode);
|
||||
|
||||
if (selectedNodes.length === this.table.columns * this.table.rows) {
|
||||
tableNode.selectPrevious();
|
||||
// Delete entire table
|
||||
tableNode.remove();
|
||||
const rootNode = $getRoot();
|
||||
rootNode.selectStart();
|
||||
return;
|
||||
}
|
||||
|
||||
selectedNodes.forEach((cellNode) => {
|
||||
if ($isElementNode(cellNode)) {
|
||||
const paragraphNode = $createParagraphNode();
|
||||
const textNode = $createTextNode();
|
||||
paragraphNode.append(textNode);
|
||||
cellNode.append(paragraphNode);
|
||||
cellNode.getChildren().forEach((child) => {
|
||||
if (child !== paragraphNode) {
|
||||
child.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$updateDOMForSelection(editor, this.table, null);
|
||||
|
||||
$setSelection(null);
|
||||
|
||||
editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
|
||||
});
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user