-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();
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) {
}
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() || [];
--- /dev/null
+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
$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);
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;
}
}
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);
}
}
}
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
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 = {
);
listenToCommonEvents(editor);
+ handleDropEvents(editor);
setEditorContentFromHtml(editor, htmlContent);
- 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
- 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