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);
+ };
+ }, []);
+}