feat: add swipeable sidebar drawer for anime details on mobile
This commit is contained in:
110
static/anime.ts
110
static/anime.ts
@@ -90,5 +90,115 @@ const initThemesDialog = (): void => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initAnimeSidebarDrawer = (): void => {
|
||||||
|
onReady(() => {
|
||||||
|
const drawer = document.querySelector<HTMLElement>("[data-anime-sidebar-drawer]");
|
||||||
|
const openButton = document.querySelector<HTMLButtonElement>("[data-anime-sidebar-open]");
|
||||||
|
const closeButton = document.querySelector<HTMLButtonElement>("[data-anime-sidebar-close]");
|
||||||
|
const backdrop = document.querySelector<HTMLButtonElement>("[data-anime-sidebar-backdrop]");
|
||||||
|
if (!drawer || !openButton || !closeButton || !backdrop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
let activeTouchId: number | null = null;
|
||||||
|
let isOpen = false;
|
||||||
|
|
||||||
|
const syncState = (): void => {
|
||||||
|
drawer.classList.toggle("hidden", !isOpen);
|
||||||
|
drawer.classList.toggle("translate-x-0", isOpen);
|
||||||
|
drawer.classList.toggle("translate-x-full", !isOpen);
|
||||||
|
backdrop.classList.toggle("hidden", !isOpen);
|
||||||
|
document.body.classList.toggle("overflow-hidden", isOpen);
|
||||||
|
openButton.setAttribute("aria-expanded", String(isOpen));
|
||||||
|
};
|
||||||
|
|
||||||
|
const open = (): void => {
|
||||||
|
isOpen = true;
|
||||||
|
syncState();
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = (): void => {
|
||||||
|
isOpen = false;
|
||||||
|
syncState();
|
||||||
|
};
|
||||||
|
|
||||||
|
openButton.addEventListener("click", open);
|
||||||
|
closeButton.addEventListener("click", close);
|
||||||
|
backdrop.addEventListener("click", close);
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape" && isOpen) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getTouch = (event: TouchEvent): Touch | null => {
|
||||||
|
if (activeTouchId === null) {
|
||||||
|
return event.touches[0] ?? event.changedTouches[0] ?? null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
Array.from(event.touches).find((touch) => touch.identifier === activeTouchId) ??
|
||||||
|
Array.from(event.changedTouches).find((touch) => touch.identifier === activeTouchId) ??
|
||||||
|
null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
"touchstart",
|
||||||
|
(event) => {
|
||||||
|
if (window.innerWidth >= 1024) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const touch = event.touches[0];
|
||||||
|
if (!touch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isOpen && touch.clientX < window.innerWidth - 28) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activeTouchId = touch.identifier;
|
||||||
|
startX = touch.clientX;
|
||||||
|
startY = touch.clientY;
|
||||||
|
},
|
||||||
|
{ passive: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
"touchend",
|
||||||
|
(event) => {
|
||||||
|
if (window.innerWidth >= 1024 || activeTouchId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const touch = getTouch(event);
|
||||||
|
activeTouchId = null;
|
||||||
|
if (!touch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaX = touch.clientX - startX;
|
||||||
|
const deltaY = Math.abs(touch.clientY - startY);
|
||||||
|
if (deltaY > 60) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOpen && startX >= window.innerWidth - 28 && deltaX < -48) {
|
||||||
|
open();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOpen && deltaX > 48) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ passive: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
syncState();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
initSynopsisToggle();
|
initSynopsisToggle();
|
||||||
initThemesDialog();
|
initThemesDialog();
|
||||||
|
initAnimeSidebarDrawer();
|
||||||
|
|||||||
@@ -46,7 +46,22 @@
|
|||||||
<div class="grow lg:max-w-4xl">
|
<div class="grow lg:max-w-4xl">
|
||||||
{{template "anime_synopsis" dict "Anime" $anime}}
|
{{template "anime_synopsis" dict "Anime" $anime}}
|
||||||
</div>
|
</div>
|
||||||
<aside class="fixed right-0 top-0 hidden h-screen w-80 shrink-0 flex-col overflow-y-auto border-l border-border-light bg-background-sidebar p-8 lg:flex">
|
<button type="button" data-anime-sidebar-open class="fixed right-0 top-1/2 z-90 flex h-14 -translate-y-1/2 items-center gap-2 border border-r-0 border-border-light bg-background-sidebar px-3 text-sm text-foreground-muted shadow-card transition-colors hover:text-foreground lg:hidden" aria-controls="anime-sidebar-drawer" aria-expanded="false">
|
||||||
|
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M9 18l6-6-6-6" />
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Open anime details</span>
|
||||||
|
</button>
|
||||||
|
<div data-anime-sidebar-drawer class="fixed inset-x-0 top-16 bottom-0 z-100 hidden w-full translate-x-full flex-col bg-background-sidebar shadow-card transition-transform duration-300 lg:static lg:z-auto lg:flex lg:h-screen lg:w-80 lg:translate-x-0 lg:overflow-y-auto lg:border-l lg:border-border-light lg:p-8">
|
||||||
|
<aside class="flex h-full flex-col overflow-y-auto p-8 lg:h-auto lg:p-0">
|
||||||
|
<div class="mb-6 flex items-center justify-between lg:hidden">
|
||||||
|
<h3 class="text-base font-normal text-foreground">Details</h3>
|
||||||
|
<button type="button" data-anime-sidebar-close class="inline-flex size-9 items-center justify-center text-foreground-muted transition-colors hover:text-foreground" aria-label="Close anime details">
|
||||||
|
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M18 6 6 18M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="flex flex-col gap-8">
|
<div class="flex flex-col gap-8">
|
||||||
<section>
|
<section>
|
||||||
<h3 class="mb-6 text-base font-normal text-foreground">Information</h3>
|
<h3 class="mb-6 text-base font-normal text-foreground">Information</h3>
|
||||||
@@ -178,7 +193,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
</div>
|
||||||
|
<button type="button" data-anime-sidebar-backdrop class="fixed inset-0 z-90 hidden bg-black/30 lg:hidden" aria-label="Close anime details"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user