From 5f07f31c9fc21c4f82b757eb2e78027ff3ad6337 Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Sun, 15 Dec 2024 14:03:08 +0000
Subject: [PATCH] Lexical: Added mobile toolbar support

Adds dynamic and fixed (out of DOM order) positioning with location
adjustment depending on space.
Also adds smarter hiding to prevent disappearing when mouse leaves but
within the same space as the toggle.
---
 .../ui/framework/blocks/button-with-menu.ts   |  1 +
 .../ui/framework/blocks/dropdown-button.ts    |  3 ++
 .../wysiwyg/ui/framework/helpers/dropdowns.ts | 52 +++++++++++++++++--
 resources/js/wysiwyg/ui/toolbars.ts           |  4 +-
 resources/sass/_editor.scss                   |  8 +++
 resources/sass/_pages.scss                    |  1 +
 6 files changed, 62 insertions(+), 7 deletions(-)

diff --git a/resources/js/wysiwyg/ui/framework/blocks/button-with-menu.ts b/resources/js/wysiwyg/ui/framework/blocks/button-with-menu.ts
index 30dd237f6..2aec7c335 100644
--- a/resources/js/wysiwyg/ui/framework/blocks/button-with-menu.ts
+++ b/resources/js/wysiwyg/ui/framework/blocks/button-with-menu.ts
@@ -16,6 +16,7 @@ export class EditorButtonWithMenu extends EditorContainerUiElement {
             button: {label: 'Menu', icon: caretDownIcon},
             showOnHover: false,
             direction: 'vertical',
+            showAside: false,
         }, menuItems);
         this.addChildren(this.dropdownButton);
     }
diff --git a/resources/js/wysiwyg/ui/framework/blocks/dropdown-button.ts b/resources/js/wysiwyg/ui/framework/blocks/dropdown-button.ts
index cba141f6c..d7f02d573 100644
--- a/resources/js/wysiwyg/ui/framework/blocks/dropdown-button.ts
+++ b/resources/js/wysiwyg/ui/framework/blocks/dropdown-button.ts
@@ -7,12 +7,14 @@ import {EditorMenuButton} from "./menu-button";
 export type EditorDropdownButtonOptions = {
     showOnHover?: boolean;
     direction?: 'vertical'|'horizontal';
+    showAside?: boolean;
     button: EditorBasicButtonDefinition|EditorButton;
 };
 
 const defaultOptions: EditorDropdownButtonOptions = {
     showOnHover: false,
     direction: 'horizontal',
+    showAside: undefined,
     button: {label: 'Menu'},
 }
 
@@ -65,6 +67,7 @@ export class EditorDropdownButton extends EditorContainerUiElement {
 
         handleDropdown({toggle: button, menu : menu,
             showOnHover: this.options.showOnHover,
+            showAside: typeof this.options.showAside === 'boolean' ? this.options.showAside : (this.options.direction === 'vertical'),
             onOpen : () => {
             this.open = true;
             this.getContext().manager.triggerStateUpdateForElement(this.button);
diff --git a/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts b/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts
index e8cef3c8d..ccced6858 100644
--- a/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts
+++ b/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts
@@ -1,20 +1,48 @@
-
-
-
 interface HandleDropdownParams {
     toggle: HTMLElement;
     menu: HTMLElement;
     showOnHover?: boolean,
     onOpen?: Function | undefined;
     onClose?: Function | undefined;
+    showAside?: boolean;
+}
+
+function positionMenu(menu: HTMLElement, toggle: HTMLElement, showAside: boolean) {
+    const toggleRect = toggle.getBoundingClientRect();
+    const menuBounds = menu.getBoundingClientRect();
+
+    menu.style.position = 'fixed';
+
+    if (showAside) {
+        let targetLeft = toggleRect.right;
+        const isRightOOB = toggleRect.right + menuBounds.width > window.innerWidth;
+        if (isRightOOB) {
+            targetLeft = Math.max(toggleRect.left - menuBounds.width, 0);
+        }
+
+        menu.style.top = toggleRect.top + 'px';
+        menu.style.left = targetLeft + 'px';
+    } else {
+        const isRightOOB = toggleRect.left + menuBounds.width > window.innerWidth;
+        let targetLeft = toggleRect.left;
+        if (isRightOOB) {
+            targetLeft = Math.max(toggleRect.right - menuBounds.width, 0);
+        }
+
+        menu.style.top = toggleRect.bottom + 'px';
+        menu.style.left = targetLeft + 'px';
+    }
 }
 
 export function handleDropdown(options: HandleDropdownParams) {
-    const {menu, toggle, onClose, onOpen, showOnHover} = options;
+    const {menu, toggle, onClose, onOpen, showOnHover, showAside} = options;
     let clickListener: Function|null = null;
 
     const hide = () => {
         menu.hidden = true;
+        menu.style.removeProperty('position');
+        menu.style.removeProperty('left');
+        menu.style.removeProperty('top');
         if (clickListener) {
             window.removeEventListener('click', clickListener as EventListener);
         }
@@ -25,6 +53,7 @@ export function handleDropdown(options: HandleDropdownParams) {
 
     const show = () => {
         menu.hidden = false
+        positionMenu(menu, toggle, Boolean(showAside));
         clickListener = (event: MouseEvent) => {
             if (!toggle.contains(event.target as HTMLElement) && !menu.contains(event.target as HTMLElement)) {
                 hide();
@@ -44,5 +73,18 @@ export function handleDropdown(options: HandleDropdownParams) {
         toggle.addEventListener('mouseenter', toggleShowing);
     }
 
-    menu.parentElement?.addEventListener('mouseleave', hide);
+    menu.parentElement?.addEventListener('mouseleave', (event: MouseEvent) => {
+
+        // Prevent mouseleave hiding if withing the same bounds of the toggle.
+        // Avoids hiding in the event the mouse is interrupted by a high z-index
+        // item like a browser scrollbar.
+        const toggleBounds = toggle.getBoundingClientRect();
+        const withinX = event.clientX <= toggleBounds.right && event.clientX >= toggleBounds.left;
+        const withinY = event.clientY <= toggleBounds.bottom && event.clientY >= toggleBounds.top;
+        const withinToggle = withinX && withinY;
+
+        if (!withinToggle) {
+            hide();
+        }
+    });
 }
\ No newline at end of file
diff --git a/resources/js/wysiwyg/ui/toolbars.ts b/resources/js/wysiwyg/ui/toolbars.ts
index 35146e5a4..886e1394b 100644
--- a/resources/js/wysiwyg/ui/toolbars.ts
+++ b/resources/js/wysiwyg/ui/toolbars.ts
@@ -149,8 +149,8 @@ export function getMainEditorFullToolbar(context: EditorUiContext): EditorContai
         new EditorOverflowContainer(4, [
             new EditorButton(link),
 
-            new EditorDropdownButton({button: table, direction: 'vertical'}, [
-                new EditorDropdownButton({button: {label: 'Insert', format: 'long'}, showOnHover: true}, [
+            new EditorDropdownButton({button: table, direction: 'vertical', showAside: false}, [
+                new EditorDropdownButton({button: {label: 'Insert', format: 'long'}, showOnHover: true, showAside: true}, [
                     new EditorTableCreator(),
                 ]),
                 new EditorSeparator(),
diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss
index bdf6ea44c..e48131837 100644
--- a/resources/sass/_editor.scss
+++ b/resources/sass/_editor.scss
@@ -24,6 +24,14 @@
   @include mixins.lightDark(border-color, #DDD, #000);
 }
 
+@include mixins.smaller-than(vars.$bp-xl) {
+  .editor-toolbar-main {
+    overflow-x: scroll;
+    flex-wrap: nowrap;
+    justify-content: start;
+  }
+}
+
 body.editor-is-fullscreen {
   overflow: hidden;
   .edit-area {
diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss
index 17bcfcfbf..45e58ffc8 100755
--- a/resources/sass/_pages.scss
+++ b/resources/sass/_pages.scss
@@ -26,6 +26,7 @@
   width: 100%;
   border-radius: 8px;
   box-shadow: vars.$bs-card;
+  min-width: 300px;
   @include mixins.lightDark(background-color, #FFF, #333)
 }