pocketbase/ui/src/components/base/Toggler.svelte

215 lines
5.6 KiB
Svelte
Raw Normal View History

2022-07-07 05:19:05 +08:00
<script>
import { onMount, createEventDispatcher } from "svelte";
import { fly } from "svelte/transition";
export let trigger = undefined;
export let active = false;
export let escClose = true;
2023-03-17 01:21:16 +08:00
export let autoScroll = true;
2022-07-07 05:19:05 +08:00
export let closableClass = "closable";
let classes = "";
export { classes as class }; // export reserved keyword
2023-03-17 01:21:16 +08:00
let container;
let containerChild;
let activeTrigger;
let scrollTimeoutId;
let hideTimeoutId;
2023-03-17 01:21:16 +08:00
let isOutsideMouseDown = false;
2022-07-07 05:19:05 +08:00
const dispatch = createEventDispatcher();
2022-10-30 16:28:14 +08:00
$: if (container) {
bindTrigger(trigger);
}
2022-07-07 05:19:05 +08:00
$: if (active) {
2022-10-30 16:28:14 +08:00
activeTrigger?.classList?.add("active");
2022-07-07 05:19:05 +08:00
dispatch("show");
} else {
2022-10-30 16:28:14 +08:00
activeTrigger?.classList?.remove("active");
2022-07-07 05:19:05 +08:00
dispatch("hide");
}
export function hideWithDelay(delay = 0) {
if (!active) {
return;
}
clearTimeout(hideTimeoutId);
hideTimeoutId = setTimeout(hide, delay);
}
2022-07-07 05:19:05 +08:00
export function hide() {
if (!active) {
return; // already hidden
}
2022-07-07 05:19:05 +08:00
active = false;
2023-03-17 01:21:16 +08:00
isOutsideMouseDown = false;
clearTimeout(scrollTimeoutId);
clearTimeout(hideTimeoutId);
2022-07-07 05:19:05 +08:00
}
export function show() {
clearTimeout(hideTimeoutId);
clearTimeout(scrollTimeoutId);
if (active) {
return; // already active
}
2022-07-07 05:19:05 +08:00
active = true;
2023-03-17 01:21:16 +08:00
// focus toggler container not nested into the trigger
if (!activeTrigger?.contains(container)) {
container?.focus();
}
2023-03-17 01:21:16 +08:00
scrollTimeoutId = setTimeout(() => {
if (!autoScroll) {
return;
}
if (containerChild?.scrollIntoViewIfNeeded) {
containerChild?.scrollIntoViewIfNeeded();
} else if (containerChild?.scrollIntoView) {
containerChild?.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
}
}, 180);
2022-07-07 05:19:05 +08:00
}
export function toggle() {
if (active) {
hide();
} else {
show();
}
}
function isClosable(elem) {
return (
!container ||
elem.classList.contains(closableClass) ||
(container.contains(elem) && elem.closest && elem.closest("." + closableClass))
);
}
function bindTrigger(newTrigger) {
cleanup();
2022-10-30 16:28:14 +08:00
container?.addEventListener("click", handleContainerClick);
container?.addEventListener("keydown", handleContainerKeydown);
activeTrigger = newTrigger || container?.parentNode;
activeTrigger?.addEventListener("click", handleTriggerClick);
activeTrigger?.addEventListener("keydown", handleTriggerKeydown);
}
function cleanup() {
clearTimeout(scrollTimeoutId);
clearTimeout(hideTimeoutId);
container?.removeEventListener("click", handleContainerClick);
container?.removeEventListener("keydown", handleContainerKeydown);
activeTrigger?.removeEventListener("click", handleTriggerClick);
activeTrigger?.removeEventListener("keydown", handleTriggerKeydown);
}
// toggler container handlers
// ---------------------------------------------------------------
function handleContainerClick(e) {
e.stopPropagation(); // prevents firing the trigger click event in case it is nested
if (isClosable(e.target)) {
hide();
2022-07-07 05:19:05 +08:00
}
}
function handleContainerKeydown(e) {
if (e.code === "Enter" || e.code === "Space") {
e.stopPropagation(); // prevents firing the trigger keydown event in case it is nested
if (isClosable(e.target)) {
// hide with a short delay since the button on:click events
// doesn't fire if the element is not visible
hideWithDelay(150);
}
}
}
// trigger handlers
// ---------------------------------------------------------------
function handleTriggerClick(e) {
e.preventDefault();
e.stopPropagation();
toggle();
}
function handleTriggerKeydown(e) {
if (e.code === "Enter" || e.code === "Space") {
2022-07-07 05:19:05 +08:00
e.preventDefault();
e.stopPropagation();
toggle();
}
}
function handleFocusChange(e) {
if (active && !activeTrigger?.contains(e.target) && !container?.contains(e.target)) {
toggle();
}
}
2023-03-17 01:21:16 +08:00
function handleEscPress(e) {
if (active && escClose && e.code === "Escape") {
e.preventDefault();
2022-07-07 05:19:05 +08:00
hide();
}
}
2023-03-17 01:21:16 +08:00
function handleOutsideMousedown(e) {
if (!active) {
return;
2023-03-17 01:21:16 +08:00
}
isOutsideMouseDown = !container?.contains(e.target);
2023-03-17 01:21:16 +08:00
}
function handleOutsideClick(e) {
if (
active &&
isOutsideMouseDown &&
2023-03-17 01:21:16 +08:00
!container?.contains(e.target) &&
!activeTrigger?.contains(e.target) &&
!e.target?.closest(".flatpickr-calendar")
2023-03-17 01:21:16 +08:00
) {
2022-07-07 05:19:05 +08:00
hide();
}
}
2022-10-30 16:28:14 +08:00
onMount(() => {
bindTrigger();
2022-07-07 05:19:05 +08:00
2022-10-30 16:28:14 +08:00
return () => cleanup();
2022-07-07 05:19:05 +08:00
});
</script>
2023-03-17 01:21:16 +08:00
<svelte:window
on:click={handleOutsideClick}
on:mousedown={handleOutsideMousedown}
2023-03-17 01:21:16 +08:00
on:keydown={handleEscPress}
on:focusin={handleFocusChange}
/>
2022-07-07 05:19:05 +08:00
<div bind:this={container} class="toggler-container" tabindex="-1" role="menu">
2022-07-07 05:19:05 +08:00
{#if active}
<div bind:this={containerChild} class={classes} class:active transition:fly={{ duration: 150, y: 3 }}>
2022-07-07 05:19:05 +08:00
<slot />
</div>
{/if}
</div>