diff --git a/apps/client/src/layouts/mobile_layout.tsx b/apps/client/src/layouts/mobile_layout.tsx index a1dd88e17..e75a7b6ba 100644 --- a/apps/client/src/layouts/mobile_layout.tsx +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -154,7 +154,7 @@ export default class MobileLayout { .child(new NoteListWidget(false)) .child() ) - .child(new MobileEditorToolbar()) + .child() ) ) .child( diff --git a/apps/client/src/widgets/type_widgets/ckeditor/mobile_editor_toolbar.css b/apps/client/src/widgets/type_widgets/ckeditor/mobile_editor_toolbar.css new file mode 100644 index 000000000..06dccd699 --- /dev/null +++ b/apps/client/src/widgets/type_widgets/ckeditor/mobile_editor_toolbar.css @@ -0,0 +1,52 @@ +.classic-toolbar-outer-container { + contain: none !important; +} + +.classic-toolbar-outer-container.visible { + height: 38px; + background-color: var(--main-background-color); + position: relative; + overflow: visible; + flex-shrink: 0; +} + +#root-widget.virtual-keyboard-opened .classic-toolbar-outer-container.ios { + position: absolute; + left: 0; + right: 0; + bottom: 0; +} + +.classic-toolbar-widget { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 38px; + overflow: scroll; + display: flex; + align-items: flex-end; + user-select: none; +} + +.classic-toolbar-widget::-webkit-scrollbar { + height: 0 !important; + width: 0 !important; +} + +.classic-toolbar-widget.dropdown-active { + height: 50vh; +} + +.classic-toolbar-widget .ck.ck-toolbar { + --ck-color-toolbar-background: transparent; + --ck-color-button-default-background: transparent; + --ck-color-button-default-disabled-background: transparent; + position: absolute; + background-color: transparent; + border: none; +} + +.classic-toolbar-widget .ck.ck-button.ck-disabled { + opacity: 0.3; +} \ No newline at end of file diff --git a/apps/client/src/widgets/type_widgets/ckeditor/mobile_editor_toolbar.ts b/apps/client/src/widgets/type_widgets/ckeditor/mobile_editor_toolbar.ts deleted file mode 100644 index 6db0b472a..000000000 --- a/apps/client/src/widgets/type_widgets/ckeditor/mobile_editor_toolbar.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { isIOS } from "../../../services/utils.js"; -import NoteContextAwareWidget from "../../note_context_aware_widget.js"; - -const TPL = /*html*/`\ -
-
-
- - -`; - -/** - * Handles the editing toolbar for CKEditor in mobile mode. The toolbar acts as a floating bar, with two different mechanism: - * - * - On iOS, because it does not respect the viewport meta value `interactive-widget=resizes-content`, we need to listen to window resizes and scroll and reposition the toolbar using absolute positioning. - * - 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 class MobileEditorToolbar extends NoteContextAwareWidget { - - private observer: MutationObserver; - private $innerWrapper!: JQuery; - - constructor() { - super(); - this.observer = new MutationObserver((e) => this.#onDropdownStateChanged(e)); - } - - get name() { - return "classicEditor"; - } - - doRender() { - this.$widget = $(TPL); - this.$innerWrapper = this.$widget.find(".classic-toolbar-widget"); - this.contentSized(); - - // 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. - this.observer.disconnect(); - this.observer.observe(this.$widget[0], { - attributeFilter: ["aria-expanded"], - subtree: true - }); - - if (isIOS()) { - this.#handlePositioningOniOS(); - } - } - - #handlePositioningOniOS() { - const adjustPosition = () => { - let bottom = window.innerHeight - (window.visualViewport?.height || 0); - this.$widget.css("bottom", `${bottom}px`); - } - - this.$widget.addClass("ios"); - window.visualViewport?.addEventListener("resize", adjustPosition); - window.addEventListener("scroll", adjustPosition); - } - - #onDropdownStateChanged(e: MutationRecord[]) { - const dropdownActive = e.map((e) => (e.target as any).ariaExpanded === "true").reduce((acc, e) => acc && e); - this.$innerWrapper.toggleClass("dropdown-active", dropdownActive); - } - - async #shouldDisplay() { - if (!this.note || this.note.type !== "text") { - return false; - } - - if (await this.noteContext?.isReadOnly()) { - return false; - } - - return true; - } - - async refreshWithNote() { - this.toggleExt(await this.#shouldDisplay()); - } - -} diff --git a/apps/client/src/widgets/type_widgets/ckeditor/mobile_editor_toolbar.tsx b/apps/client/src/widgets/type_widgets/ckeditor/mobile_editor_toolbar.tsx new file mode 100644 index 000000000..71965761b --- /dev/null +++ b/apps/client/src/widgets/type_widgets/ckeditor/mobile_editor_toolbar.tsx @@ -0,0 +1,67 @@ +import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks"; +import { useNoteContext } from "../../react/hooks"; +import "./mobile_editor_toolbar.css"; +import { isIOS } from "../../../services/utils"; + +/** + * Handles the editing toolbar for CKEditor in mobile mode. The toolbar acts as a floating bar, with two different mechanism: + * + * - On iOS, because it does not respect the viewport meta value `interactive-widget=resizes-content`, we need to listen to window resizes and scroll and reposition the toolbar using absolute positioning. + * - 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 [ shouldDisplay, setShouldDisplay ] = useState(false); + const [ dropdownActive, setDropdownActive ] = useState(false); + + usePositioningOniOS(wrapperRef); + + useEffect(() => { + noteContext?.isReadOnly().then(isReadOnly => { + setShouldDisplay(note?.type === "text" && !isReadOnly); + }); + }, [ note ]); + + // 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; + + const observer = new MutationObserver(e => { + setDropdownActive(e.map((e) => (e.target as any).ariaExpanded === "true").reduce((acc, e) => acc && e)); + }); + + observer.observe(wrapperRef.current, { + attributeFilter: ["aria-expanded"], + subtree: true + }); + + return () => observer.disconnect(); + }, []); + + return ( +
+
+
+ ) +} + +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`; + }, []); + + useEffect(() => { + if (!isIOS()) return; + + window.visualViewport?.addEventListener("resize", adjustPosition); + window.addEventListener("scroll", adjustPosition); + + return () => { + window.visualViewport?.removeEventListener("resize", adjustPosition); + window.removeEventListener("scroll", adjustPosition); + }; + }, []); +}