mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 23:29:02 +02:00
feat(react/widgets): port mobile editor toolbar
This commit is contained in:
parent
d579e39b40
commit
aa4375e25f
@ -154,7 +154,7 @@ export default class MobileLayout {
|
|||||||
.child(new NoteListWidget(false))
|
.child(new NoteListWidget(false))
|
||||||
.child(<FilePropertiesWrapper />)
|
.child(<FilePropertiesWrapper />)
|
||||||
)
|
)
|
||||||
.child(new MobileEditorToolbar())
|
.child(<MobileEditorToolbar />)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -1,130 +0,0 @@
|
|||||||
import { isIOS } from "../../../services/utils.js";
|
|
||||||
import NoteContextAwareWidget from "../../note_context_aware_widget.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`\
|
|
||||||
<div class="classic-toolbar-outer-container">
|
|
||||||
<div class="classic-toolbar-widget"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<HTMLElement>;
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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<HTMLDivElement>(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 (
|
||||||
|
<div className={`classic-toolbar-outer-container ${!shouldDisplay ? "hidden-ext" : "visible"} ${isIOS() ? "ios" : ""}`}>
|
||||||
|
<div ref={wrapperRef} className={`classic-toolbar-widget ${dropdownActive ? "dropdown-active" : ""}`}></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePositioningOniOS(wrapperRef: MutableRef<HTMLDivElement | null>) {
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user