mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-19 18:22:16 +03: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
3.3 KiB
TypeScript
127 lines
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;
|
|
} |