mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-29 06:09:56 +00:00

- Added handling to not include parent of top-most list range selection so that it's not also changed while not visually part of the selection range. - Fixed issue where list items could be left over after unnesting, due to empty checks/removals occuring before all child handling. - Added node sorting, applied to list items during nest operations so that selection range remains reliable.
127 lines
No EOL
3.3 KiB
TypeScript
127 lines
No EOL
3.3 KiB
TypeScript
import {
|
|
$createParagraphNode,
|
|
$getRoot,
|
|
$isDecoratorNode,
|
|
$isElementNode, $isRootNode,
|
|
$isTextNode,
|
|
ElementNode,
|
|
LexicalEditor,
|
|
LexicalNode
|
|
} from "lexical";
|
|
import {LexicalNodeMatcher} from "../nodes";
|
|
import {$generateNodesFromDOM} from "@lexical/html";
|
|
import {htmlToDom} from "./dom";
|
|
import {NodeHasAlignment, NodeHasInset} from "lexical/nodes/common";
|
|
import {$findMatchingParent} from "@lexical/utils";
|
|
|
|
function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] {
|
|
return nodes.map(node => {
|
|
if ($isTextNode(node)) {
|
|
const paragraph = $createParagraphNode();
|
|
paragraph.append(node);
|
|
return paragraph;
|
|
}
|
|
return node;
|
|
});
|
|
}
|
|
|
|
export function $htmlToBlockNodes(editor: LexicalEditor, html: string): LexicalNode[] {
|
|
const dom = htmlToDom(html);
|
|
const nodes = $generateNodesFromDOM(editor, dom);
|
|
return wrapTextNodes(nodes);
|
|
}
|
|
|
|
export function $getParentOfType(node: LexicalNode, matcher: LexicalNodeMatcher): LexicalNode | null {
|
|
for (const parent of node.getParents()) {
|
|
if (matcher(parent)) {
|
|
return parent;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function $getAllNodesOfType(matcher: LexicalNodeMatcher, root?: ElementNode): LexicalNode[] {
|
|
if (!root) {
|
|
root = $getRoot();
|
|
}
|
|
|
|
const matches = [];
|
|
|
|
for (const child of root.getChildren()) {
|
|
if (matcher(child)) {
|
|
matches.push(child);
|
|
}
|
|
|
|
if ($isElementNode(child)) {
|
|
matches.push(...$getAllNodesOfType(matcher, child));
|
|
}
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
/**
|
|
* Get the nearest root/block level node for the given position.
|
|
*/
|
|
export function $getNearestBlockNodeForCoords(editor: LexicalEditor, x: number, y: number): LexicalNode | null {
|
|
// TODO - Take into account x for floated blocks?
|
|
const rootNodes = $getRoot().getChildren();
|
|
for (const node of rootNodes) {
|
|
const nodeDom = editor.getElementByKey(node.__key);
|
|
if (!nodeDom) {
|
|
continue;
|
|
}
|
|
|
|
const bounds = nodeDom.getBoundingClientRect();
|
|
if (y <= bounds.bottom) {
|
|
return node;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function $getNearestNodeBlockParent(node: LexicalNode): LexicalNode|null {
|
|
const isBlockNode = (node: LexicalNode): boolean => {
|
|
return ($isElementNode(node) || $isDecoratorNode(node)) && !node.isInline() && !$isRootNode(node);
|
|
};
|
|
|
|
if (isBlockNode(node)) {
|
|
return node;
|
|
}
|
|
|
|
return $findMatchingParent(node, isBlockNode);
|
|
}
|
|
|
|
export function $sortNodes(nodes: LexicalNode[]): LexicalNode[] {
|
|
const idChain: string[] = [];
|
|
const addIds = (n: ElementNode) => {
|
|
for (const child of n.getChildren()) {
|
|
idChain.push(child.getKey())
|
|
if ($isElementNode(child)) {
|
|
addIds(child)
|
|
}
|
|
}
|
|
};
|
|
|
|
const root = $getRoot();
|
|
addIds(root);
|
|
|
|
const sorted = Array.from(nodes);
|
|
sorted.sort((a, b) => {
|
|
const aIndex = idChain.indexOf(a.getKey());
|
|
const bIndex = idChain.indexOf(b.getKey());
|
|
return aIndex - bIndex;
|
|
});
|
|
|
|
return sorted;
|
|
}
|
|
|
|
export function nodeHasAlignment(node: object): node is NodeHasAlignment {
|
|
return '__alignment' in node;
|
|
}
|
|
|
|
export function nodeHasInset(node: object): node is NodeHasInset {
|
|
return '__inset' in node;
|
|
} |