1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2026-01-03 23:42:28 +03:00

Lexical: Split helpers to utils, refactored files

This commit is contained in:
Dan Brown
2024-08-03 18:14:01 +01:00
parent e94ad78ea7
commit efec752985
39 changed files with 152 additions and 136 deletions

View File

@@ -0,0 +1,69 @@
import {$getRoot, $getSelection, LexicalEditor} from "lexical";
import {$generateHtmlFromNodes} from "@lexical/html";
import {$htmlToBlockNodes} from "./nodes";
export function setEditorContentFromHtml(editor: LexicalEditor, html: string) {
editor.update(() => {
// Empty existing
const root = $getRoot();
for (const child of root.getChildren()) {
child.remove(true);
}
const nodes = $htmlToBlockNodes(editor, html);
root.append(...nodes);
});
}
export function appendHtmlToEditor(editor: LexicalEditor, html: string) {
editor.update(() => {
const root = $getRoot();
const nodes = $htmlToBlockNodes(editor, html);
root.append(...nodes);
});
}
export function prependHtmlToEditor(editor: LexicalEditor, html: string) {
editor.update(() => {
const root = $getRoot();
const nodes = $htmlToBlockNodes(editor, html);
let reference = root.getChildren()[0];
for (let i = nodes.length - 1; i >= 0; i--) {
if (reference) {
reference.insertBefore(nodes[i]);
} else {
root.append(nodes[i])
}
reference = nodes[i];
}
});
}
export function insertHtmlIntoEditor(editor: LexicalEditor, html: string) {
editor.update(() => {
const selection = $getSelection();
const nodes = $htmlToBlockNodes(editor, html);
const reference = selection?.getNodes()[0];
const referencesParents = reference?.getParents() || [];
const topLevel = referencesParents[referencesParents.length - 1];
if (topLevel && reference) {
for (let i = nodes.length - 1; i >= 0; i--) {
reference.insertAfter(nodes[i]);
}
}
});
}
export function getEditorContentAsHtml(editor: LexicalEditor): Promise<string> {
return new Promise((resolve, reject) => {
editor.getEditorState().read(() => {
const html = $generateHtmlFromNodes(editor);
resolve(html);
});
});
}
export function focusEditor(editor: LexicalEditor) {
editor.focus(() => {}, {defaultSelection: "rootStart"});
}

View File

@@ -0,0 +1,24 @@
export function el(tag: string, attrs: Record<string, string | null> = {}, children: (string | HTMLElement)[] = []): HTMLElement {
const el = document.createElement(tag);
const attrKeys = Object.keys(attrs);
for (const attr of attrKeys) {
if (attrs[attr] !== null) {
el.setAttribute(attr, attrs[attr] as string);
}
}
for (const child of children) {
if (typeof child === 'string') {
el.append(document.createTextNode(child));
} else {
el.append(child);
}
}
return el;
}
export function htmlToDom(html: string): Document {
const parser = new DOMParser();
return parser.parseFromString(html, 'text/html');
}

View File

@@ -0,0 +1,53 @@
import {$getRoot, $isTextNode, LexicalEditor, LexicalNode} from "lexical";
import {LexicalNodeMatcher} from "../nodes";
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
import {$generateNodesFromDOM} from "@lexical/html";
import {htmlToDom} from "./dom";
function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] {
return nodes.map(node => {
if ($isTextNode(node)) {
const paragraph = $createCustomParagraphNode();
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;
}
/**
* 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;
}

View File

@@ -0,0 +1,140 @@
import {
$createNodeSelection,
$createParagraphNode,
$getRoot,
$getSelection,
$isElementNode,
$isTextNode,
$setSelection,
BaseSelection,
ElementFormatType,
ElementNode,
LexicalNode,
TextFormatType
} from "lexical";
import {$findMatchingParent, $getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes";
import {$setBlocksType} from "@lexical/selection";
import {$getParentOfType} from "./nodes";
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, $createParagraphNode);
} 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);
}
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 $selectionContainsElementFormat(selection: BaseSelection | null, format: ElementFormatType): boolean {
const nodes = $getBlockElementNodesInSelection(selection);
for (const node of nodes) {
if (node.getFormatType() === format) {
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 = $findMatchingParent(node, (node) => {
return $isElementNode(node) && !node.isInline();
}) as ElementNode | null;
if (blockElement) {
blockNodes.set(blockElement.getKey(), blockElement);
}
}
return Array.from(blockNodes.values());
}