diff --git a/resources/js/wysiwyg/services/drop-paste-handling.ts b/resources/js/wysiwyg/services/drop-paste-handling.ts index 57f9a80ae..b63c57ada 100644 --- a/resources/js/wysiwyg/services/drop-paste-handling.ts +++ b/resources/js/wysiwyg/services/drop-paste-handling.ts @@ -1,40 +1,35 @@ import { $createParagraphNode, $insertNodes, - $isDecoratorNode, COMMAND_PRIORITY_HIGH, DROP_COMMAND, + $isDecoratorNode, $isTextNode, $setSelection, COMMAND_PRIORITY_HIGH, DROP_COMMAND, LexicalEditor, LexicalNode, PASTE_COMMAND } from "lexical"; -import {$insertNewBlockNodesAtSelection, $selectSingleNode} from "../utils/selection"; -import {$getNearestBlockNodeForCoords, $htmlToBlockNodes} from "../utils/nodes"; +import {$insertNewNodesAtSelection, $selectSingleNode} from "../utils/selection"; +import {$getNodePositionFromMouseEvent, $htmlToBlockNodes} from "../utils/nodes"; import {Clipboard} from "../../services/clipboard"; import {$createImageNode} from "@lexical/rich-text/LexicalImageNode"; import {$createLinkNode} from "@lexical/link"; import {EditorImageData, uploadImageFile} from "../utils/images"; import {EditorUiContext} from "../ui/framework/core"; -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); + const position = $getNodePositionFromMouseEvent(event, editor); - if (positionNode) { - $selectSingleNode(positionNode); + if (position && $isTextNode(position.node)) { + const selection = position.node.select(position.offset, position.offset); + $setSelection(selection); + } else if (position) { + $selectSingleNode(position.node); } - $insertNewBlockNodesAtSelection(nodes, true); + $insertNewNodesAtSelection(nodes); - if (!$isDecoratorNode(positionNode) || !positionNode?.getTextContent()) { - positionNode?.remove(); + if (position) { + if (!$isDecoratorNode(position.node) && !position.node?.getTextContent()) { + position.node.remove(); + } } } @@ -113,6 +108,7 @@ function handleImageLinkInsert(data: DataTransfer, context: EditorUiContext): bo function createDropListener(context: EditorUiContext): (event: DragEvent) => boolean { const editor = context.editor; return (event: DragEvent): boolean => { + // Template handling const templateId = event.dataTransfer?.getData('bookstack/template') || ''; if (templateId) { diff --git a/resources/js/wysiwyg/utils/nodes.ts b/resources/js/wysiwyg/utils/nodes.ts index ed70bf699..eab8203a0 100644 --- a/resources/js/wysiwyg/utils/nodes.ts +++ b/resources/js/wysiwyg/utils/nodes.ts @@ -1,5 +1,6 @@ import { $createParagraphNode, + $getNearestNodeFromDOMNode, $getRoot, $isDecoratorNode, $isElementNode, $isRootNode, @@ -64,6 +65,27 @@ export function $getAllNodesOfType(matcher: LexicalNodeMatcher, root?: ElementNo return matches; } +/** + * Get the node based on the given mouse event. + */ +export function $getNodePositionFromMouseEvent(event: MouseEvent, editor: LexicalEditor): {node: LexicalNode, offset: number}|null { + const x = event.clientX; + const y = event.clientY; + const caretPosition = window.document.caretPositionFromPoint(event.x, event.y); + if (!caretPosition) { + const backup = $getNearestBlockNodeForCoords(editor, x, y); + return backup ? {node: backup, offset: 0} : null; + } + + const node = $getNearestNodeFromDOMNode(caretPosition.offsetNode); + if (!node) { + const backup = $getNearestBlockNodeForCoords(editor, x, y); + return backup ? {node: backup, offset: 0} : null; + } + + return {node, offset: caretPosition.offset}; +} + /** * Get the nearest root/block level node for the given position. */ diff --git a/resources/js/wysiwyg/utils/selection.ts b/resources/js/wysiwyg/utils/selection.ts index e4b5bf2dc..f5d3699fc 100644 --- a/resources/js/wysiwyg/utils/selection.ts +++ b/resources/js/wysiwyg/utils/selection.ts @@ -109,6 +109,45 @@ export function $insertNewBlockNodesAtSelection(nodes: LexicalNode[], insertAfte } } +export function $insertNewNodesAtSelection(nodes: LexicalNode[]) { + const selection = $getSelection(); + const selectionPoints = selection?.getStartEndPoints(); + let target: LexicalNode|null = null; + let targetBlock: LexicalNode|null = null; + if (selectionPoints) { + const selectionEnd = selectionPoints[1]; + target = selectionEnd.getNode(); + targetBlock = target ? $getNearestNodeBlockParent(target) : null; + } + + for (const node of nodes) { + const isBlock = $isBlockElementNode(node); + + if (isBlock && !targetBlock) { + // Append to the root if its a block and we can't determine position + $getRoot().append(node); + } else if (isBlock && targetBlock) { + // Insert after the target block if we have a block + targetBlock.insertAfter(node); + } else if (!isBlock && selection) { + // Insert at selection if likely inline + selection.insertNodes(nodes); + } else if (!isBlock && $isElementNode(targetBlock)) { + // Append inside the target block if inline but we don't have + // a selection (typically used by the case below) + targetBlock.append(node); + } else { + // Otherwise (where inline) create a new root level paragraph + // and insert content into that. Update the target block + // for re-use by other inline elements. + const paragraph = $createParagraphNode(); + paragraph.append(node); + $getRoot().append(paragraph); + targetBlock = paragraph; + } + } +} + export function $selectSingleNode(node: LexicalNode) { const nodeSelection = $createNodeSelection(); nodeSelection.add(node.getKey());