From 9bfcadd95f4c3917ff4aa4fd4b10bfa7ce4c6c09 Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Fri, 28 Mar 2025 14:17:52 +0000
Subject: [PATCH] Lexical: Improved navigation around images/media

- Added specific handling to move/insert-up/down on arrow press.
- Prevented resize overlay from interrupting image node focus.
---
 .../lexical/rich-text/LexicalImageNode.ts     |  2 +-
 .../js/wysiwyg/services/keyboard-handling.ts  | 43 +++++++++++++++++--
 resources/sass/_editor.scss                   |  2 +
 3 files changed, 43 insertions(+), 4 deletions(-)

diff --git a/resources/js/wysiwyg/lexical/rich-text/LexicalImageNode.ts b/resources/js/wysiwyg/lexical/rich-text/LexicalImageNode.ts
index 9f42ad732..40f4ab711 100644
--- a/resources/js/wysiwyg/lexical/rich-text/LexicalImageNode.ts
+++ b/resources/js/wysiwyg/lexical/rich-text/LexicalImageNode.ts
@@ -133,7 +133,7 @@ export class ImageNode extends ElementNode {
 
         element.addEventListener('click', e => {
             _editor.update(() => {
-                $selectSingleNode(this);
+                this.select();
             });
         });
 
diff --git a/resources/js/wysiwyg/services/keyboard-handling.ts b/resources/js/wysiwyg/services/keyboard-handling.ts
index ff6117b2b..0ef0b81bf 100644
--- a/resources/js/wysiwyg/services/keyboard-handling.ts
+++ b/resources/js/wysiwyg/services/keyboard-handling.ts
@@ -3,7 +3,7 @@ import {
     $createParagraphNode,
     $getSelection,
     $isDecoratorNode,
-    COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND,
+    COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND,
     KEY_BACKSPACE_COMMAND,
     KEY_DELETE_COMMAND,
     KEY_ENTER_COMMAND, KEY_TAB_COMMAND,
@@ -43,7 +43,7 @@ function deleteSingleSelectedNode(editor: LexicalEditor) {
 }
 
 /**
- * Insert a new empty node after the selection if the selection contains a single
+ * Insert a new empty node before/after the selection if the selection contains a single
  * selected node (like image, media etc...).
  */
 function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
@@ -67,6 +67,37 @@ function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEve
     return false;
 }
 
+function focusAdjacentOrInsertForSingleSelectNode(editor: LexicalEditor, event: KeyboardEvent|null, after: boolean = true): boolean {
+    const selectionNodes = getLastSelection(editor)?.getNodes() || [];
+    if (!isSingleSelectedNode(selectionNodes)) {
+        return false;
+    }
+
+    event?.preventDefault();
+
+    const node = selectionNodes[0];
+    const nearestBlock = $getNearestNodeBlockParent(node) || node;
+    let target = after ? nearestBlock.getNextSibling() : nearestBlock.getPreviousSibling();
+
+    requestAnimationFrame(() => {
+        editor.update(() => {
+            if (!target) {
+                target = $createParagraphNode();
+                if (after) {
+                    nearestBlock.insertAfter(target)
+                } else {
+                    nearestBlock.insertBefore(target);
+                }
+            }
+
+            target.selectStart();
+        });
+    });
+
+
+    return true;
+}
+
 /**
  * Insert a new node after a details node, if inside a details node that's
  * the last element, and if the cursor is at the last block within the details node.
@@ -199,8 +230,13 @@ export function registerKeyboardHandling(context: EditorUiContext): () => void {
         return handleInsetOnTab(context.editor, event);
     }, COMMAND_PRIORITY_LOW);
 
+    const unregisterUp = context.editor.registerCommand(KEY_ARROW_UP_COMMAND, (event): boolean => {
+        return focusAdjacentOrInsertForSingleSelectNode(context.editor, event, false);
+    }, COMMAND_PRIORITY_LOW);
+
     const unregisterDown = context.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (event): boolean => {
-        return insertAfterDetails(context.editor, event);
+        return insertAfterDetails(context.editor, event)
+            || focusAdjacentOrInsertForSingleSelectNode(context.editor, event, true)
     }, COMMAND_PRIORITY_LOW);
 
     return () => {
@@ -208,6 +244,7 @@ export function registerKeyboardHandling(context: EditorUiContext): () => void {
         unregisterDelete();
         unregisterEnter();
         unregisterTab();
+        unregisterUp();
         unregisterDown();
     };
 }
\ No newline at end of file
diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss
index 9f7694e85..35f11c5a2 100644
--- a/resources/sass/_editor.scss
+++ b/resources/sass/_editor.scss
@@ -370,8 +370,10 @@ body.editor-is-fullscreen {
   display: inline-block;
   outline: 2px dashed var(--editor-color-primary);
   direction: ltr;
+  pointer-events: none;
 }
 .editor-node-resizer-handle {
+  pointer-events: auto;
   position: absolute;
   display: block;
   width: 10px;