Lexical: Started drop handling, handled templates
This commit is contained in:
parent
ce8c9dd079
commit
9a7edc6e52
|
@ -1,30 +1,10 @@
|
||||||
import {$getRoot, $getSelection, $isTextNode, LexicalEditor, LexicalNode, RootNode} from "lexical";
|
import {$getRoot, $getSelection, LexicalEditor} from "lexical";
|
||||||
import {$generateHtmlFromNodes, $generateNodesFromDOM} from "@lexical/html";
|
import {$generateHtmlFromNodes} from "@lexical/html";
|
||||||
import {$createCustomParagraphNode} from "./nodes/custom-paragraph";
|
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) {
|
export function setEditorContentFromHtml(editor: LexicalEditor, html: string) {
|
||||||
const dom = htmlToDom(html);
|
|
||||||
|
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
// Empty existing
|
// Empty existing
|
||||||
const root = $getRoot();
|
const root = $getRoot();
|
||||||
|
@ -32,27 +12,23 @@ export function setEditorContentFromHtml(editor: LexicalEditor, html: string) {
|
||||||
child.remove(true);
|
child.remove(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodes = $generateNodesFromDOM(editor, dom);
|
const nodes = $htmlToBlockNodes(editor, html);
|
||||||
root.append(...wrapTextNodes(nodes));
|
root.append(...nodes);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function appendHtmlToEditor(editor: LexicalEditor, html: string) {
|
export function appendHtmlToEditor(editor: LexicalEditor, html: string) {
|
||||||
const dom = htmlToDom(html);
|
|
||||||
|
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const root = $getRoot();
|
const root = $getRoot();
|
||||||
const nodes = $generateNodesFromDOM(editor, dom);
|
const nodes = $htmlToBlockNodes(editor, html);
|
||||||
root.append(...wrapTextNodes(nodes));
|
root.append(...nodes);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prependHtmlToEditor(editor: LexicalEditor, html: string) {
|
export function prependHtmlToEditor(editor: LexicalEditor, html: string) {
|
||||||
const dom = htmlToDom(html);
|
|
||||||
|
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const root = $getRoot();
|
const root = $getRoot();
|
||||||
const nodes = wrapTextNodes($generateNodesFromDOM(editor, dom));
|
const nodes = $htmlToBlockNodes(editor, html);
|
||||||
let reference = root.getChildren()[0];
|
let reference = root.getChildren()[0];
|
||||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||||
if (reference) {
|
if (reference) {
|
||||||
|
@ -66,10 +42,9 @@ export function prependHtmlToEditor(editor: LexicalEditor, html: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function insertHtmlIntoEditor(editor: LexicalEditor, html: string) {
|
export function insertHtmlIntoEditor(editor: LexicalEditor, html: string) {
|
||||||
const dom = htmlToDom(html);
|
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection();
|
||||||
const nodes = wrapTextNodes($generateNodesFromDOM(editor, dom));
|
const nodes = $htmlToBlockNodes(editor, html);
|
||||||
|
|
||||||
const reference = selection?.getNodes()[0];
|
const reference = selection?.getNodes()[0];
|
||||||
const referencesParents = reference?.getParents() || [];
|
const referencesParents = reference?.getParents() || [];
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -3,12 +3,14 @@ import {
|
||||||
$createParagraphNode, $getRoot,
|
$createParagraphNode, $getRoot,
|
||||||
$getSelection, $isElementNode,
|
$getSelection, $isElementNode,
|
||||||
$isTextNode, $setSelection,
|
$isTextNode, $setSelection,
|
||||||
BaseSelection, ElementFormatType, ElementNode,
|
BaseSelection, ElementFormatType, ElementNode, LexicalEditor,
|
||||||
LexicalNode, TextFormatType
|
LexicalNode, TextFormatType
|
||||||
} from "lexical";
|
} from "lexical";
|
||||||
import {LexicalElementNodeCreator, LexicalNodeMatcher} from "./nodes";
|
import {LexicalElementNodeCreator, LexicalNodeMatcher} from "./nodes";
|
||||||
import {$findMatchingParent, $getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
|
import {$findMatchingParent, $getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
|
||||||
import {$setBlocksType} from "@lexical/selection";
|
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 {
|
export function el(tag: string, attrs: Record<string, string|null> = {}, children: (string|HTMLElement)[] = []): HTMLElement {
|
||||||
const el = document.createElement(tag);
|
const el = document.createElement(tag);
|
||||||
|
@ -30,6 +32,28 @@ export function el(tag: string, attrs: Record<string, string|null> = {}, childre
|
||||||
return el;
|
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 {
|
export function $selectionContainsNodeType(selection: BaseSelection|null, matcher: LexicalNodeMatcher): boolean {
|
||||||
return $getNodeFromSelection(selection, matcher) !== null;
|
return $getNodeFromSelection(selection, matcher) !== null;
|
||||||
}
|
}
|
||||||
|
@ -88,17 +112,25 @@ export function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creat
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $insertNewBlockNodeAtSelection(node: LexicalNode, insertAfter: boolean = true) {
|
export function $insertNewBlockNodeAtSelection(node: LexicalNode, insertAfter: boolean = true) {
|
||||||
|
$insertNewBlockNodesAtSelection([node], insertAfter);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $insertNewBlockNodesAtSelection(nodes: LexicalNode[], insertAfter: boolean = true) {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection();
|
||||||
const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
|
const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
|
||||||
|
|
||||||
if (blockElement) {
|
if (blockElement) {
|
||||||
if (insertAfter) {
|
if (insertAfter) {
|
||||||
blockElement.insertAfter(node);
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||||
|
blockElement.insertAfter(nodes[i]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
blockElement.insertBefore(node);
|
for (const node of nodes) {
|
||||||
|
blockElement.insertBefore(node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$getRoot().append(node);
|
$getRoot().append(...nodes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,4 +183,25 @@ export function $getBlockElementNodesInSelection(selection: BaseSelection|null):
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(blockNodes.values());
|
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;
|
||||||
}
|
}
|
|
@ -9,6 +9,7 @@ import {registerTableResizer} from "./ui/framework/helpers/table-resizer";
|
||||||
import {el} from "./helpers";
|
import {el} from "./helpers";
|
||||||
import {EditorUiContext} from "./ui/framework/core";
|
import {EditorUiContext} from "./ui/framework/core";
|
||||||
import {listen as listenToCommonEvents} from "./common-events";
|
import {listen as listenToCommonEvents} from "./common-events";
|
||||||
|
import {handleDropEvents} from "./drop-handling";
|
||||||
|
|
||||||
export function createPageEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
|
export function createPageEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
|
||||||
const config: CreateEditorArgs = {
|
const config: CreateEditorArgs = {
|
||||||
|
@ -49,6 +50,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
|
||||||
);
|
);
|
||||||
|
|
||||||
listenToCommonEvents(editor);
|
listenToCommonEvents(editor);
|
||||||
|
handleDropEvents(editor);
|
||||||
|
|
||||||
setEditorContentFromHtml(editor, htmlContent);
|
setEditorContentFromHtml(editor, htmlContent);
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
- Keyboard shortcuts support
|
- Keyboard shortcuts support
|
||||||
- Draft/change management (connect with page editor component)
|
- Draft/change management (connect with page editor component)
|
||||||
- Add ID support to all block types
|
- Add ID support to all block types
|
||||||
- Template drag & drop / insert
|
|
||||||
- Video attachment drop / insert
|
- Video attachment drop / insert
|
||||||
- Task list render/import from existing format
|
- Task list render/import from existing format
|
||||||
- Link popup menu for cross-content reference
|
- 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.
|
- 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
|
- 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.
|
- `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).
|
Loading…
Reference in New Issue