From 9b3ca654922e86986cc880e7e35bc5b5a945c140 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 22 Nov 2025 11:37:34 +0200 Subject: [PATCH] fix(text): classic toolbar broken on mobile due to prior change --- .../src/widgets/ribbon/FormattingToolbar.tsx | 9 +++- .../type_widgets/text/EditableText.tsx | 26 ---------- .../text/mobile_editor_toolbar.tsx | 49 ++++++++++++++++--- 3 files changed, 48 insertions(+), 36 deletions(-) diff --git a/apps/client/src/widgets/ribbon/FormattingToolbar.tsx b/apps/client/src/widgets/ribbon/FormattingToolbar.tsx index f25234ead..47963b2bf 100644 --- a/apps/client/src/widgets/ribbon/FormattingToolbar.tsx +++ b/apps/client/src/widgets/ribbon/FormattingToolbar.tsx @@ -10,16 +10,21 @@ import { TabContext } from "./ribbon-interface"; * The ribbon item is active by default for text notes, as long as they are not in read-only mode. * * ! The toolbar is not only used in the ribbon, but also in the quick edit feature. + * * The mobile toolbar is handled separately (see `MobileEditorToolbar`). */ export default function FormattingToolbar({ hidden, ntxId }: TabContext) { const containerRef = useRef(null); const [ textNoteEditorType ] = useTriliumOption("textNoteEditorType"); + // Attach the toolbar from the CKEditor. useTriliumEvent("textEditorRefreshed", ({ ntxId: eventNtxId, editor }) => { - if (eventNtxId !== ntxId) return; + if (eventNtxId !== ntxId || !containerRef.current) return; const toolbar = editor.ui.view.toolbar?.element; - if (toolbar && containerRef.current) { + + if (toolbar) { containerRef.current.replaceChildren(toolbar); + } else { + containerRef.current.replaceChildren(); } }); diff --git a/apps/client/src/widgets/type_widgets/text/EditableText.tsx b/apps/client/src/widgets/type_widgets/text/EditableText.tsx index 490eba95d..9b3915abd 100644 --- a/apps/client/src/widgets/type_widgets/text/EditableText.tsx +++ b/apps/client/src/widgets/type_widgets/text/EditableText.tsx @@ -233,12 +233,6 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext onWatchdogStateChange={onWatchdogStateChange} onChange={() => spacedUpdate.scheduleUpdate()} onEditorInitialized={(editor) => { - console.log("Editor has been initialized!", parentComponent, editor); - - if (isClassicEditor) { - setupClassicEditor(editor, parentComponent); - } - if (hasTouchBar) { const handler = () => refreshTouchBarRef.current?.(); for (const event of [ "bold", "italic", "underline", "paragraph", "heading" ]) { @@ -303,26 +297,6 @@ function onNotificationWarning(data, evt) { evt.stop(); } -function setupClassicEditor(editor: CKTextEditor, parentComponent: Component | undefined) { - if (!parentComponent) return; - - if (utils.isMobile()) { - // Reposition all dropdowns to point upwards instead of downwards. - // See https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for more info. - const toolbarView = (editor as ClassicEditor).ui.view.toolbar; - for (const item of toolbarView.items) { - if (!("panelView" in item)) continue; - - item.on("change:isOpen", () => { - if (!("isOpen" in item) || !item.isOpen) return; - - // @ts-ignore - item.panelView.position = item.panelView.position.replace("s", "n"); - }); - } - } -} - function EditableTextTouchBar({ watchdogRef, refreshTouchBarRef }: { watchdogRef: RefObject, refreshTouchBarRef: RefObject<() => void> }) { const [ headingSelectedIndex, setHeadingSelectedIndex ] = useState(); diff --git a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx index 71965761b..4b2ac7e1c 100644 --- a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx +++ b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx @@ -1,7 +1,8 @@ import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks"; -import { useNoteContext } from "../../react/hooks"; +import { useNoteContext, useTriliumEvent } from "../../react/hooks"; import "./mobile_editor_toolbar.css"; import { isIOS } from "../../../services/utils"; +import { CKTextEditor, ClassicEditor } from "@triliumnext/ckeditor5"; /** * Handles the editing toolbar for CKEditor in mobile mode. The toolbar acts as a floating bar, with two different mechanism: @@ -10,12 +11,12 @@ import { isIOS } from "../../../services/utils"; * - On Android, the viewport change makes the keyboard resize the content area, all we have to do is to hide the tab bar and global menu (handled in the global style). */ export default function MobileEditorToolbar() { - const wrapperRef = useRef(null); - const { note, noteContext } = useNoteContext(); + const containerRef = useRef(null); + const { note, noteContext, ntxId } = useNoteContext(); const [ shouldDisplay, setShouldDisplay ] = useState(false); const [ dropdownActive, setDropdownActive ] = useState(false); - usePositioningOniOS(wrapperRef); + usePositioningOniOS(containerRef); useEffect(() => { noteContext?.isReadOnly().then(isReadOnly => { @@ -23,15 +24,28 @@ export default function MobileEditorToolbar() { }); }, [ note ]); + // Attach the toolbar from the CKEditor. + useTriliumEvent("textEditorRefreshed", ({ ntxId: eventNtxId, editor }) => { + if (eventNtxId !== ntxId || !containerRef.current) return; + const toolbar = editor.ui.view.toolbar?.element; + + repositionDropdowns(editor); + if (toolbar) { + containerRef.current.replaceChildren(toolbar); + } else { + containerRef.current.replaceChildren(); + } + }); + // Observe when a dropdown is expanded to apply a style that allows the dropdown to be visible, since we can't have the element both visible and the toolbar scrollable. useEffect(() => { - if (!wrapperRef.current) return; + if (!containerRef.current) return; const observer = new MutationObserver(e => { setDropdownActive(e.map((e) => (e.target as any).ariaExpanded === "true").reduce((acc, e) => acc && e)); }); - observer.observe(wrapperRef.current, { + observer.observe(containerRef.current, { attributeFilter: ["aria-expanded"], subtree: true }); @@ -41,7 +55,7 @@ export default function MobileEditorToolbar() { return (
-
+
) } @@ -50,7 +64,7 @@ function usePositioningOniOS(wrapperRef: MutableRef) { const adjustPosition = useCallback(() => { if (!wrapperRef.current) return; let bottom = window.innerHeight - (window.visualViewport?.height || 0); - wrapperRef.current.style.bottom = `${bottom}px`; + wrapperRef.current.style.bottom = `${bottom}px`; }, []); useEffect(() => { @@ -65,3 +79,22 @@ function usePositioningOniOS(wrapperRef: MutableRef) { }; }, []); } + +/** + * Reposition all dropdowns to point upwards instead of downwards. + * See https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for more info. + * @param editor + */ +function repositionDropdowns(editor: CKTextEditor) { + const toolbarView = (editor as ClassicEditor).ui.view.toolbar; + for (const item of toolbarView.items) { + if (!("panelView" in item)) continue; + + item.on("change:isOpen", () => { + if (!("isOpen" in item) || !item.isOpen) return; + + // @ts-ignore + item.panelView.position = item.panelView.position.replace("s", "n"); + }); + } +}