]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Started drop handling, handled templates
authorDan Brown <redacted>
Mon, 29 Jul 2024 14:27:41 +0000 (15:27 +0100)
committerDan Brown <redacted>
Mon, 29 Jul 2024 14:27:41 +0000 (15:27 +0100)
resources/js/wysiwyg/actions.ts
resources/js/wysiwyg/drop-handling.ts [new file with mode: 0644]
resources/js/wysiwyg/helpers.ts
resources/js/wysiwyg/index.ts
resources/js/wysiwyg/todo.md

index 0e220252567b53b67d79208e73bce692fe727b19..a3d2f0ef60347e7ed38867c4bb03d97fa40f9664 100644 (file)
@@ -1,30 +1,10 @@
-import {$getRoot, $getSelection, $isTextNode, LexicalEditor, LexicalNode, RootNode} from "lexical";
-import {$generateHtmlFromNodes, $generateNodesFromDOM} from "@lexical/html";
-import {$createCustomParagraphNode} from "./nodes/custom-paragraph";
+import {$getRoot, $getSelection, LexicalEditor} from "lexical";
+import {$generateHtmlFromNodes} from "@lexical/html";
+import {$htmlToBlockNodes} from "./helpers";
 
-function htmlToDom(html: string): Document {
-    const parser = new DOMParser();
-    return parser.parseFromString(html, 'text/html');
-}
 
-function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] {
-    return nodes.map(node => {
-        if ($isTextNode(node)) {
-            const paragraph = $createCustomParagraphNode();
-            paragraph.append(node);
-            return paragraph;
-        }
-        return node;
-    });
-}
-
-function appendNodesToRoot(root: RootNode, nodes: LexicalNode[]) {
-    root.append(...wrapTextNodes(nodes));
-}
 
 export function setEditorContentFromHtml(editor: LexicalEditor, html: string) {
-    const dom = htmlToDom(html);
-
     editor.update(() => {
         // Empty existing
         const root = $getRoot();
@@ -32,27 +12,23 @@ export function setEditorContentFromHtml(editor: LexicalEditor, html: string) {
             child.remove(true);
         }
 
-        const nodes = $generateNodesFromDOM(editor, dom);
-        root.append(...wrapTextNodes(nodes));
+        const nodes = $htmlToBlockNodes(editor, html);
+        root.append(...nodes);
     });
 }
 
 export function appendHtmlToEditor(editor: LexicalEditor, html: string) {
-    const dom = htmlToDom(html);
-
     editor.update(() => {
         const root = $getRoot();
-        const nodes = $generateNodesFromDOM(editor, dom);
-        root.append(...wrapTextNodes(nodes));
+        const nodes = $htmlToBlockNodes(editor, html);
+        root.append(...nodes);
     });
 }
 
 export function prependHtmlToEditor(editor: LexicalEditor, html: string) {
-    const dom = htmlToDom(html);
-
     editor.update(() => {
         const root = $getRoot();
-        const nodes = wrapTextNodes($generateNodesFromDOM(editor, dom));
+        const nodes = $htmlToBlockNodes(editor, html);
         let reference = root.getChildren()[0];
         for (let i = nodes.length - 1; i >= 0; i--) {
             if (reference) {
@@ -66,10 +42,9 @@ export function prependHtmlToEditor(editor: LexicalEditor, html: string) {
 }
 
 export function insertHtmlIntoEditor(editor: LexicalEditor, html: string) {
-    const dom = htmlToDom(html);
     editor.update(() => {
         const selection = $getSelection();
-        const nodes = wrapTextNodes($generateNodesFromDOM(editor, dom));
+        const nodes = $htmlToBlockNodes(editor, html);
 
         const reference = selection?.getNodes()[0];
         const referencesParents = reference?.getParents() || [];
diff --git a/resources/js/wysiwyg/drop-handling.ts b/resources/js/wysiwyg/drop-handling.ts
new file mode 100644 (file)
index 0000000..92dc758
--- /dev/null
@@ -0,0 +1,71 @@
+import {
+    $getNearestNodeFromDOMNode,
+    $getRoot,
+    $insertNodes,
+    $isDecoratorNode,
+    LexicalEditor,
+    LexicalNode
+} from "lexical";
+import {
+    $getNearestBlockNodeForCoords,
+    $htmlToBlockNodes,
+    $insertNewBlockNodeAtSelection, $insertNewBlockNodesAtSelection,
+    $selectSingleNode
+} from "./helpers";
+
+function $getNodeFromMouseEvent(event: MouseEvent, editor: LexicalEditor): LexicalNode|null {
+    const x = event.clientX;
+    const y = event.clientY;
+    const dom = document.elementFromPoint(x, y);
+    if (!dom) {
+        return null;
+    }
+
+    return $getNearestBlockNodeForCoords(editor, event.clientX, event.clientY);
+}
+
+function $insertNodesAtEvent(nodes: LexicalNode[], event: DragEvent, editor: LexicalEditor) {
+    const positionNode = $getNodeFromMouseEvent(event, editor);
+
+    if (positionNode) {
+        $selectSingleNode(positionNode);
+    }
+
+    $insertNewBlockNodesAtSelection(nodes, true);
+
+    if (!$isDecoratorNode(positionNode) || !positionNode?.getTextContent()) {
+        positionNode?.remove();
+    }
+}
+
+async function insertTemplateToEditor(editor: LexicalEditor, templateId: string, event: DragEvent) {
+    const resp = await window.$http.get(`/templates/${templateId}`);
+    const data = (resp.data || {html: ''}) as {html: string}
+    const html: string = data.html || '';
+
+    editor.update(() => {
+        const newNodes = $htmlToBlockNodes(editor, html);
+        $insertNodesAtEvent(newNodes, event, editor);
+    });
+}
+
+function createDropListener(editor: LexicalEditor): (event: DragEvent) => void {
+    return (event: DragEvent) => {
+        // Template handling
+        const templateId = event.dataTransfer?.getData('bookstack/template') || '';
+        if (templateId) {
+            event.preventDefault();
+            insertTemplateToEditor(editor, templateId, event);
+            return;
+        }
+    };
+}
+
+export function handleDropEvents(editor: LexicalEditor) {
+    const dropListener = createDropListener(editor);
+
+    editor.registerRootListener((rootElement, prevRootElement) => {
+        rootElement?.addEventListener('drop', dropListener);
+        prevRootElement?.removeEventListener('drop', dropListener);
+    });
+}
\ No newline at end of file
index 6a55c429cda154ae9e94196b740ad0b16df85e6e..07755f449e658d2f0f3d8730aae7141266b0de87 100644 (file)
@@ -3,12 +3,14 @@ import {
     $createParagraphNode, $getRoot,
     $getSelection, $isElementNode,
     $isTextNode, $setSelection,
-    BaseSelection, ElementFormatType, ElementNode,
+    BaseSelection, ElementFormatType, ElementNode, LexicalEditor,
     LexicalNode, TextFormatType
 } from "lexical";
 import {LexicalElementNodeCreator, LexicalNodeMatcher} from "./nodes";
 import {$findMatchingParent, $getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
 import {$setBlocksType} from "@lexical/selection";
+import {$createCustomParagraphNode} from "./nodes/custom-paragraph";
+import {$generateNodesFromDOM} from "@lexical/html";
 
 export function el(tag: string, attrs: Record<string, string|null> = {}, children: (string|HTMLElement)[] = []): HTMLElement {
     const el = document.createElement(tag);
@@ -30,6 +32,28 @@ export function el(tag: string, attrs: Record<string, string|null> = {}, childre
     return el;
 }
 
+function htmlToDom(html: string): Document {
+    const parser = new DOMParser();
+    return parser.parseFromString(html, 'text/html');
+}
+
+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 $selectionContainsNodeType(selection: BaseSelection|null, matcher: LexicalNodeMatcher): boolean {
     return $getNodeFromSelection(selection, matcher) !== null;
 }
@@ -88,17 +112,25 @@ export function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creat
 }
 
 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) {
-            blockElement.insertAfter(node);
+            for (let i = nodes.length - 1; i >= 0; i--) {
+                blockElement.insertAfter(nodes[i]);
+            }
         } else {
-            blockElement.insertBefore(node);
+            for (const node of nodes) {
+                blockElement.insertBefore(node);
+            }
         }
     } else {
-        $getRoot().append(node);
+        $getRoot().append(...nodes);
     }
 }
 
@@ -151,4 +183,25 @@ export function $getBlockElementNodesInSelection(selection: BaseSelection|null):
     }
 
     return Array.from(blockNodes.values());
+}
+
+/**
+ * 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;
 }
\ No newline at end of file
index e53b9b057dd416f1995ac326037b7151c71a3b1c..fee5365728eed4b44fcba4a9cc09fe65aa057b17 100644 (file)
@@ -9,6 +9,7 @@ import {registerTableResizer} from "./ui/framework/helpers/table-resizer";
 import {el} from "./helpers";
 import {EditorUiContext} from "./ui/framework/core";
 import {listen as listenToCommonEvents} from "./common-events";
+import {handleDropEvents} from "./drop-handling";
 
 export function createPageEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
     const config: CreateEditorArgs = {
@@ -49,6 +50,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
     );
 
     listenToCommonEvents(editor);
+    handleDropEvents(editor);
 
     setEditorContentFromHtml(editor, htmlContent);
 
index 49f685beabfc7db60b85e98f7052dfa7e95100ba..5d495e7d83d63911f3a2433536393b0265b9abf9 100644 (file)
@@ -13,7 +13,6 @@
 - Keyboard shortcuts support
 - Draft/change management (connect with page editor component)
 - Add ID support to all block types
-- Template drag & drop / insert
 - Video attachment drop / insert
 - Task list render/import from existing format
 - Link popup menu for cross-content reference
@@ -28,4 +27,5 @@
 
 - Image resizing currently bugged, maybe change to ghost resizer in decorator instead of updating core node.
 - Removing link around image via button deletes image, not just link 
-- `SELECTION_CHANGE_COMMAND` not fired when clicking out of a table cell. Prevents toolbar hiding on table unselect.
\ No newline at end of file
+- `SELECTION_CHANGE_COMMAND` not fired when clicking out of a table cell. Prevents toolbar hiding on table unselect.
+- Template drag/drop not handled when outside core editor area (ignored in margin area).
\ No newline at end of file