mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-12-13 14:33:37 +08:00
662110c269
Added list nesting support to allow li > ul style nesting which lexical didn't do by default. Adds tab handling for inset/outset controls. Will be a range of edge-case bugs to squash during testing.
224 lines
6.5 KiB
TypeScript
224 lines
6.5 KiB
TypeScript
import {
|
|
$createNodeSelection,
|
|
$createParagraphNode, $createRangeSelection,
|
|
$getRoot,
|
|
$getSelection, $isDecoratorNode,
|
|
$isElementNode,
|
|
$isTextNode,
|
|
$setSelection,
|
|
BaseSelection, DecoratorNode,
|
|
ElementFormatType,
|
|
ElementNode, LexicalEditor,
|
|
LexicalNode,
|
|
TextFormatType, TextNode
|
|
} from "lexical";
|
|
import {$findMatchingParent, $getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
|
|
import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes";
|
|
import {$setBlocksType} from "@lexical/selection";
|
|
|
|
import {$getNearestNodeBlockParent, $getParentOfType, nodeHasAlignment} from "./nodes";
|
|
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
|
|
import {CommonBlockAlignment} from "../nodes/_common";
|
|
|
|
const lastSelectionByEditor = new WeakMap<LexicalEditor, BaseSelection|null>;
|
|
|
|
export function getLastSelection(editor: LexicalEditor): BaseSelection|null {
|
|
return lastSelectionByEditor.get(editor) || null;
|
|
}
|
|
|
|
export function setLastSelection(editor: LexicalEditor, selection: BaseSelection|null): void {
|
|
lastSelectionByEditor.set(editor, selection);
|
|
}
|
|
|
|
export function $selectionContainsNodeType(selection: BaseSelection | null, matcher: LexicalNodeMatcher): boolean {
|
|
return $getNodeFromSelection(selection, matcher) !== null;
|
|
}
|
|
|
|
export function $getNodeFromSelection(selection: BaseSelection | null, matcher: LexicalNodeMatcher): LexicalNode | null {
|
|
if (!selection) {
|
|
return null;
|
|
}
|
|
|
|
for (const node of selection.getNodes()) {
|
|
if (matcher(node)) {
|
|
return node;
|
|
}
|
|
|
|
const matchedParent = $getParentOfType(node, matcher);
|
|
if (matchedParent) {
|
|
return matchedParent;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function $selectionContainsTextFormat(selection: BaseSelection | null, format: TextFormatType): boolean {
|
|
if (!selection) {
|
|
return false;
|
|
}
|
|
|
|
for (const node of selection.getNodes()) {
|
|
if ($isTextNode(node) && node.hasFormat(format)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creator: LexicalElementNodeCreator) {
|
|
const selection = $getSelection();
|
|
const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
|
|
if (selection && matcher(blockElement)) {
|
|
$setBlocksType(selection, $createCustomParagraphNode);
|
|
} else {
|
|
$setBlocksType(selection, creator);
|
|
}
|
|
}
|
|
|
|
export function $insertNewBlockNodeAtSelection(node: LexicalNode, insertAfter: boolean = true) {
|
|
$insertNewBlockNodesAtSelection([node], insertAfter);
|
|
}
|
|
|
|
export function $insertNewBlockNodesAtSelection(nodes: LexicalNode[], insertAfter: boolean = true) {
|
|
const selection = $getSelection();
|
|
const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
|
|
|
|
if (blockElement) {
|
|
if (insertAfter) {
|
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
blockElement.insertAfter(nodes[i]);
|
|
}
|
|
} else {
|
|
for (const node of nodes) {
|
|
blockElement.insertBefore(node);
|
|
}
|
|
}
|
|
} else {
|
|
$getRoot().append(...nodes);
|
|
}
|
|
}
|
|
|
|
export function $selectSingleNode(node: LexicalNode) {
|
|
const nodeSelection = $createNodeSelection();
|
|
nodeSelection.add(node.getKey());
|
|
$setSelection(nodeSelection);
|
|
}
|
|
|
|
function getFirstTextNodeInNodes(nodes: LexicalNode[]): TextNode|null {
|
|
for (const node of nodes) {
|
|
if ($isTextNode(node)) {
|
|
return node;
|
|
}
|
|
|
|
if ($isElementNode(node)) {
|
|
const children = node.getChildren();
|
|
const textNode = getFirstTextNodeInNodes(children);
|
|
if (textNode !== null) {
|
|
return textNode;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getLastTextNodeInNodes(nodes: LexicalNode[]): TextNode|null {
|
|
const revNodes = [...nodes].reverse();
|
|
for (const node of revNodes) {
|
|
if ($isTextNode(node)) {
|
|
return node;
|
|
}
|
|
|
|
if ($isElementNode(node)) {
|
|
const children = [...node.getChildren()].reverse();
|
|
const textNode = getLastTextNodeInNodes(children);
|
|
if (textNode !== null) {
|
|
return textNode;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function $selectNodes(nodes: LexicalNode[]) {
|
|
if (nodes.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const selection = $createRangeSelection();
|
|
const firstText = getFirstTextNodeInNodes(nodes);
|
|
const lastText = getLastTextNodeInNodes(nodes);
|
|
if (firstText && lastText) {
|
|
selection.setTextNodeRange(firstText, 0, lastText, lastText.getTextContentSize() || 0)
|
|
$setSelection(selection);
|
|
}
|
|
}
|
|
|
|
export function $toggleSelection(editor: LexicalEditor) {
|
|
const lastSelection = getLastSelection(editor);
|
|
|
|
if (lastSelection) {
|
|
window.requestAnimationFrame(() => {
|
|
editor.update(() => {
|
|
$setSelection(lastSelection.clone());
|
|
})
|
|
});
|
|
}
|
|
}
|
|
|
|
export function $selectionContainsNode(selection: BaseSelection | null, node: LexicalNode): boolean {
|
|
if (!selection) {
|
|
return false;
|
|
}
|
|
|
|
const key = node.getKey();
|
|
for (const node of selection.getNodes()) {
|
|
if (node.getKey() === key) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function $selectionContainsAlignment(selection: BaseSelection | null, alignment: CommonBlockAlignment): boolean {
|
|
|
|
const nodes = [
|
|
...(selection?.getNodes() || []),
|
|
...$getBlockElementNodesInSelection(selection)
|
|
];
|
|
for (const node of nodes) {
|
|
if (nodeHasAlignment(node) && node.getAlignment() === alignment) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function $getBlockElementNodesInSelection(selection: BaseSelection | null): ElementNode[] {
|
|
if (!selection) {
|
|
return [];
|
|
}
|
|
|
|
const blockNodes: Map<string, ElementNode> = new Map();
|
|
for (const node of selection.getNodes()) {
|
|
const blockElement = $getNearestNodeBlockParent(node);
|
|
if ($isElementNode(blockElement)) {
|
|
blockNodes.set(blockElement.getKey(), blockElement);
|
|
}
|
|
}
|
|
|
|
return Array.from(blockNodes.values());
|
|
}
|
|
|
|
export function $getDecoratorNodesInSelection(selection: BaseSelection | null): DecoratorNode<any>[] {
|
|
if (!selection) {
|
|
return [];
|
|
}
|
|
|
|
return selection.getNodes().filter(node => $isDecoratorNode(node));
|
|
} |