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 {$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() || [];
|
||||
|
|
|
@ -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,
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,3 +184,24 @@ 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;
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -29,3 +28,4 @@
|
|||
- 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.
|
||||
- Template drag/drop not handled when outside core editor area (ignored in margin area).
|
Loading…
Reference in New Issue