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:
69
resources/js/wysiwyg/utils/actions.ts
Normal file
69
resources/js/wysiwyg/utils/actions.ts
Normal 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"});
|
||||
}
|
||||
24
resources/js/wysiwyg/utils/dom.ts
Normal file
24
resources/js/wysiwyg/utils/dom.ts
Normal 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');
|
||||
}
|
||||
53
resources/js/wysiwyg/utils/nodes.ts
Normal file
53
resources/js/wysiwyg/utils/nodes.ts
Normal 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;
|
||||
}
|
||||
140
resources/js/wysiwyg/utils/selection.ts
Normal file
140
resources/js/wysiwyg/utils/selection.ts
Normal 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());
|
||||
}
|
||||
Reference in New Issue
Block a user