Notes/src/public/app/widgets/mobile_widgets/sidebar_container.ts

169 lines
6.9 KiB
TypeScript
Raw Normal View History

import type { EventData } from "../../components/app_context.js";
import type { Screen } from "../../components/mobile_screen_switcher.js";
import BasicWidget from "../basic_widget.js";
import FlexContainer, { type FlexDirection } from "../containers/flex_container.js";
2025-01-04 00:20:22 +02:00
const DRAG_STATE_NONE = 0;
const DRAG_STATE_INITIAL_DRAG = 1;
const DRAG_STATE_DRAGGING = 2;
/** Percentage of drag that the user has to do in order for the popup to open (0-100). */
const DRAG_OPEN_THRESHOLD = 10;
/** The number of pixels the user has to drag across the screen to the right when the sidebar is closed to trigger the drag open animation. */
const DRAG_CLOSED_START_THRESHOLD = 10;
/** The number of pixels the user has to drag across the screen to the left when the sidebar is opened to trigger the drag close animation. */
const DRAG_OPENED_START_THRESHOLD = 80;
export default class SidebarContainer extends FlexContainer<BasicWidget> {
private screenName: Screen;
/** The screen name that is currently active, according to the screen changed event. */
private activeScreenName?: Screen;
private currentTranslate: number;
private dragState: number;
private startX?: number;
private translatePercentage: number;
private sidebarEl!: HTMLElement;
private backdropEl!: HTMLElement;
private originalSidebarTransition: string;
private originalBackdropTransition: string;
constructor(screenName: Screen, direction: FlexDirection) {
super(direction);
this.screenName = screenName;
this.currentTranslate = -100;
this.translatePercentage = 0;
2025-01-04 00:20:22 +02:00
this.dragState = DRAG_STATE_NONE;
this.originalSidebarTransition = "";
this.originalBackdropTransition = "";
}
doRender() {
super.doRender();
2025-01-04 00:22:16 +02:00
document.addEventListener("touchstart", (e) => this.#onDragStart(e));
document.addEventListener("touchmove", (e) => this.#onDragMove(e), { passive: false });
document.addEventListener("touchend", (e) => this.#onDragEnd(e));
}
#onDragStart(e: TouchEvent | MouseEvent) {
const x = "touches" in e ? e.touches[0].clientX : e.clientX;
this.startX = x;
2025-01-04 00:25:59 +02:00
if (x > 30 && this.currentTranslate === -100) {
return;
}
2025-01-04 01:54:01 +02:00
this.#setInitialState();
2025-01-04 00:20:22 +02:00
this.dragState = DRAG_STATE_INITIAL_DRAG;
2025-01-04 00:50:11 +02:00
this.translatePercentage = 0;
}
#onDragMove(e: TouchEvent | MouseEvent) {
if (this.dragState === DRAG_STATE_NONE || !this.startX) {
return;
}
const x = "touches" in e ? e.touches[0].clientX : e.clientX;
const deltaX = x - this.startX;
2025-01-04 00:20:22 +02:00
if (this.dragState === DRAG_STATE_INITIAL_DRAG) {
if (this.currentTranslate === -100 ? deltaX > DRAG_CLOSED_START_THRESHOLD : deltaX < -DRAG_OPENED_START_THRESHOLD) {
2025-01-04 00:50:11 +02:00
/* Disable the transitions since they affect performance, they are going to reenabled once drag ends. */
this.sidebarEl.style.transition = "none";
this.backdropEl.style.transition = "none";
this.backdropEl.style.opacity = String(this.currentTranslate === -100 ? 0 : 1);
2025-01-04 00:50:11 +02:00
this.backdropEl.classList.add("show");
2025-01-04 00:20:22 +02:00
this.dragState = DRAG_STATE_DRAGGING;
}
2025-01-04 13:21:10 +02:00
if (this.currentTranslate !== -100) {
// Return early to avoid consuming the event, this allows the user to scroll vertically.
return;
}
2025-01-04 00:20:22 +02:00
} else if (this.dragState === DRAG_STATE_DRAGGING) {
2025-01-04 00:50:11 +02:00
const width = this.sidebarEl.offsetWidth;
2025-01-04 00:20:22 +02:00
const translatePercentage = Math.min(0, Math.max(this.currentTranslate + (deltaX / width) * 100, -100));
this.translatePercentage = translatePercentage;
2025-01-04 00:50:11 +02:00
this.sidebarEl.style.transform = `translateX(${translatePercentage}%)`;
2025-01-09 18:07:02 +02:00
this.backdropEl.style.opacity = String(Math.max(0, 1 + translatePercentage / 100));
2025-01-04 00:20:22 +02:00
}
2025-01-04 13:21:10 +02:00
// Consume the event to prevent the user from doing the back to previous page gesture on iOS.
2025-01-04 00:20:22 +02:00
e.preventDefault();
}
#onDragEnd(e: TouchEvent | MouseEvent) {
2025-01-04 00:20:22 +02:00
if (this.dragState === DRAG_STATE_NONE) {
return;
}
if (this.dragState === DRAG_STATE_INITIAL_DRAG) {
this.dragState = DRAG_STATE_NONE;
return;
}
// If the sidebar is closed, snap the sidebar open only if the user swiped over a threshold.
// When the sidebar is open, always close for a smooth experience.
2025-01-09 18:07:02 +02:00
const isOpen = this.currentTranslate === -100 && this.translatePercentage > -(100 - DRAG_OPEN_THRESHOLD);
const screen = isOpen ? "tree" : "detail";
if (this.activeScreenName !== screen) {
// Trigger the set active screen command for the rest of the UI to know whether the sidebar is active or not.
// This allows the toggle sidebar button to work, by knowing the right state.
this.triggerCommand("setActiveScreen", { screen });
} else {
// If the active screen hasn't changed, usually due to the user making a very short gesture that results in the sidebar not being closed/opened,
// we need to hide the animation but setActiveScreen command will not trigger the event since we are still on the same screen.
this.#setSidebarOpen(isOpen);
}
2025-01-04 01:54:01 +02:00
}
#setInitialState() {
if (this.sidebarEl) {
// Already initialized.
return;
}
const sidebarEl = document.getElementById("mobile-sidebar-wrapper");
const backdropEl = document.getElementById("mobile-sidebar-container");
backdropEl?.addEventListener("click", () => {
2025-01-09 18:07:02 +02:00
this.triggerCommand("setActiveScreen", { screen: "detail" });
});
if (!sidebarEl || !backdropEl) {
throw new Error("Unable to find the sidebar or backdrop.");
}
this.sidebarEl = sidebarEl;
this.backdropEl = backdropEl;
2025-01-04 01:54:01 +02:00
this.originalSidebarTransition = this.sidebarEl.style.transition;
this.originalBackdropTransition = this.backdropEl.style.transition;
}
#setSidebarOpen(isOpen: boolean) {
2025-01-04 01:54:01 +02:00
if (!this.sidebarEl) {
return;
}
2025-01-04 00:50:11 +02:00
this.sidebarEl.classList.toggle("show", isOpen);
2025-01-09 18:07:02 +02:00
this.sidebarEl.style.transform = isOpen ? "translateX(0)" : "translateX(-100%)";
2025-01-04 00:50:11 +02:00
this.sidebarEl.style.transition = this.originalSidebarTransition;
2025-01-04 01:54:01 +02:00
this.backdropEl.classList.toggle("show", isOpen);
this.backdropEl.style.transition = this.originalBackdropTransition;
this.backdropEl.style.opacity = String(isOpen ? 1 : 0);
2025-01-04 00:20:22 +02:00
2025-01-04 01:54:01 +02:00
this.currentTranslate = isOpen ? 0 : -100;
2025-01-04 00:20:22 +02:00
this.dragState = DRAG_STATE_NONE;
}
2025-01-09 18:07:02 +02:00
activeScreenChangedEvent({ activeScreen }: EventData<"activeScreenChanged">) {
this.activeScreenName = activeScreen;
2025-01-04 01:54:01 +02:00
this.#setInitialState();
this.#setSidebarOpen(activeScreen === this.screenName);
}
}