From 20364382034c4979dc05e207242baf2871bf6283 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 10 Sep 2024 12:14:26 +0100 Subject: [PATCH] Lexical: Added single node enter handling Also updated media to be an inline element to align with old editor behaviour. --- resources/js/wysiwyg/nodes/media.ts | 9 +++- .../js/wysiwyg/services/keyboard-handling.ts | 52 ++++++++++++++++--- .../js/wysiwyg/ui/defaults/forms/objects.ts | 4 +- resources/js/wysiwyg/utils/nodes.ts | 23 +++++++- resources/js/wysiwyg/utils/selection.ts | 9 ++-- 5 files changed, 79 insertions(+), 18 deletions(-) diff --git a/resources/js/wysiwyg/nodes/media.ts b/resources/js/wysiwyg/nodes/media.ts index fb940f893..8658a1216 100644 --- a/resources/js/wysiwyg/nodes/media.ts +++ b/resources/js/wysiwyg/nodes/media.ts @@ -196,6 +196,10 @@ export class MediaNode extends ElementNode { return true; } + isParentRequired(): boolean { + return true; + } + createInnerDOM() { const sources = (this.__tag === 'video' || this.__tag === 'audio') ? this.__sources : []; const sourceEls = sources.map(source => el('source', source)); @@ -325,12 +329,13 @@ export function $createMediaNodeFromHtml(html: string): MediaNode | null { const videoExtensions = ['mp4', 'mpeg', 'm4v', 'm4p', 'mov']; const audioExtensions = ['3gp', 'aac', 'flac', 'mp3', 'm4a', 'ogg', 'wav', 'webm']; -const iframeExtensions = ['html', 'htm', 'php', 'asp', 'aspx']; +const iframeExtensions = ['html', 'htm', 'php', 'asp', 'aspx', '']; export function $createMediaNodeFromSrc(src: string): MediaNode { let nodeTag: MediaNodeTag = 'iframe'; const srcEnd = src.split('?')[0].split('/').pop() || ''; - const extension = (srcEnd.split('.').pop() || '').toLowerCase(); + const srcEndSplit = srcEnd.split('.'); + const extension = (srcEndSplit.length > 1 ? srcEndSplit[srcEndSplit.length - 1] : '').toLowerCase(); if (videoExtensions.includes(extension)) { nodeTag = 'video'; } else if (audioExtensions.includes(extension)) { diff --git a/resources/js/wysiwyg/services/keyboard-handling.ts b/resources/js/wysiwyg/services/keyboard-handling.ts index 7e3323f86..65a8e4254 100644 --- a/resources/js/wysiwyg/services/keyboard-handling.ts +++ b/resources/js/wysiwyg/services/keyboard-handling.ts @@ -4,22 +4,55 @@ import { COMMAND_PRIORITY_LOW, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, - LexicalEditor + KEY_ENTER_COMMAND, + LexicalEditor, + LexicalNode } from "lexical"; import {$isImageNode} from "../nodes/image"; import {$isMediaNode} from "../nodes/media"; import {getLastSelection} from "../utils/selection"; +import {$getNearestNodeBlockParent} from "../utils/nodes"; +import {$createCustomParagraphNode} from "../nodes/custom-paragraph"; + +function isSingleSelectedNode(nodes: LexicalNode[]): boolean { + if (nodes.length === 1) { + const node = nodes[0]; + if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node)) { + return true; + } + } + + return false; +} function deleteSingleSelectedNode(editor: LexicalEditor) { const selectionNodes = getLastSelection(editor)?.getNodes() || []; - if (selectionNodes.length === 1) { + if (isSingleSelectedNode(selectionNodes)) { + editor.update(() => { + selectionNodes[0].remove(); + }); + } +} + +function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean { + const selectionNodes = getLastSelection(editor)?.getNodes() || []; + if (isSingleSelectedNode(selectionNodes)) { const node = selectionNodes[0]; - if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node)) { - editor.update(() => { - node.remove(); + const nearestBlock = $getNearestNodeBlockParent(node) || node; + if (nearestBlock) { + requestAnimationFrame(() => { + editor.update(() => { + const newParagraph = $createCustomParagraphNode(); + nearestBlock.insertAfter(newParagraph); + newParagraph.select(); + }); }); + event?.preventDefault(); + return true; } } + + return false; } export function registerKeyboardHandling(context: EditorUiContext): () => void { @@ -33,8 +66,13 @@ export function registerKeyboardHandling(context: EditorUiContext): () => void { return false; }, COMMAND_PRIORITY_LOW); + const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => { + return insertAfterSingleSelectedNode(context.editor, event); + }, COMMAND_PRIORITY_LOW); + return () => { - unregisterBackspace(); - unregisterDelete(); + unregisterBackspace(); + unregisterDelete(); + unregisterEnter(); }; } \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/defaults/forms/objects.ts b/resources/js/wysiwyg/ui/defaults/forms/objects.ts index 714d5f64b..f1575953b 100644 --- a/resources/js/wysiwyg/ui/defaults/forms/objects.ts +++ b/resources/js/wysiwyg/ui/defaults/forms/objects.ts @@ -197,7 +197,7 @@ export const media: EditorFormDefinition = { if (selectedNode && node) { selectedNode.replace(node) } else if (node) { - $insertNodeToNearestRoot(node); + $insertNodes([node]); } }); @@ -213,7 +213,7 @@ export const media: EditorFormDefinition = { updateNode.setSrc(src); updateNode.setWidthAndHeight(width, height); if (!selectedNode) { - $insertNodeToNearestRoot(updateNode); + $insertNodes([updateNode]); } }); diff --git a/resources/js/wysiwyg/utils/nodes.ts b/resources/js/wysiwyg/utils/nodes.ts index e33cfda7c..b8bb8de9a 100644 --- a/resources/js/wysiwyg/utils/nodes.ts +++ b/resources/js/wysiwyg/utils/nodes.ts @@ -1,9 +1,18 @@ -import {$getRoot, $isElementNode, $isTextNode, ElementNode, LexicalEditor, LexicalNode} from "lexical"; +import { + $getRoot, + $isDecoratorNode, + $isElementNode, + $isTextNode, + ElementNode, + LexicalEditor, + LexicalNode +} from "lexical"; import {LexicalNodeMatcher} from "../nodes"; import {$createCustomParagraphNode} from "../nodes/custom-paragraph"; import {$generateNodesFromDOM} from "@lexical/html"; import {htmlToDom} from "./dom"; import {NodeHasAlignment} from "../nodes/_common"; +import {$findMatchingParent} from "@lexical/utils"; function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] { return nodes.map(node => { @@ -73,6 +82,18 @@ export function $getNearestBlockNodeForCoords(editor: LexicalEditor, x: number, return null; } +export function $getNearestNodeBlockParent(node: LexicalNode): LexicalNode|null { + const isBlockNode = (node: LexicalNode): boolean => { + return ($isElementNode(node) || $isDecoratorNode(node)) && !node.isInline(); + }; + + if (isBlockNode(node)) { + return node; + } + + return $findMatchingParent(node, isBlockNode); +} + export function nodeHasAlignment(node: object): node is NodeHasAlignment { return '__alignment' in node; } \ No newline at end of file diff --git a/resources/js/wysiwyg/utils/selection.ts b/resources/js/wysiwyg/utils/selection.ts index 4f565fa10..4aa21045f 100644 --- a/resources/js/wysiwyg/utils/selection.ts +++ b/resources/js/wysiwyg/utils/selection.ts @@ -16,7 +16,7 @@ import {$findMatchingParent, $getNearestBlockElementAncestorOrThrow} from "@lexi import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes"; import {$setBlocksType} from "@lexical/selection"; -import {$getParentOfType, nodeHasAlignment} from "./nodes"; +import {$getNearestNodeBlockParent, $getParentOfType, nodeHasAlignment} from "./nodes"; import {$createCustomParagraphNode} from "../nodes/custom-paragraph"; import {CommonBlockAlignment} from "../nodes/_common"; @@ -155,11 +155,8 @@ export function $getBlockElementNodesInSelection(selection: BaseSelection | null const blockNodes: Map = new Map(); for (const node of selection.getNodes()) { - const blockElement = $findMatchingParent(node, (node) => { - return $isElementNode(node) && !node.isInline(); - }) as ElementNode | null; - - if (blockElement) { + const blockElement = $getNearestNodeBlockParent(node); + if ($isElementNode(blockElement)) { blockNodes.set(blockElement.getKey(), blockElement); } }