mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
22d078b47f
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.
415 lines
11 KiB
TypeScript
415 lines
11 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
}
|
|
}
|