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;
|
|
|
|
export let closableClass = "closable";
|
|
|
|
let classes = "";
|
|
|
|
export { classes as class }; // export reserved keyword
|
|
|
|
|
2022-10-30 16:28:14 +08:00
|
|
|
let container = undefined;
|
|
|
|
let activeTrigger = undefined;
|
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 hide() {
|
|
|
|
active = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function show() {
|
|
|
|
active = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function toggle() {
|
|
|
|
if (active) {
|
|
|
|
hide();
|
|
|
|
} else {
|
|
|
|
show();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function isClosable(elem) {
|
|
|
|
return (
|
|
|
|
!container ||
|
|
|
|
elem.classList.contains(closableClass) ||
|
|
|
|
// is the trigger itself (or a direct child)
|
2022-10-30 16:28:14 +08:00
|
|
|
(activeTrigger?.contains(elem) && !container.contains(elem)) ||
|
2022-07-07 05:19:05 +08:00
|
|
|
// is closable toggler child
|
|
|
|
(container.contains(elem) && elem.closest && elem.closest("." + closableClass))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleClickToggle(e) {
|
|
|
|
if (!active || isClosable(e.target)) {
|
|
|
|
e.preventDefault();
|
2022-10-30 16:28:14 +08:00
|
|
|
e.stopPropagation();
|
|
|
|
|
2022-07-07 05:19:05 +08:00
|
|
|
toggle();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleKeydownToggle(e) {
|
|
|
|
if (
|
|
|
|
(e.code === "Enter" || e.code === "Space") && // enter or spacebar
|
|
|
|
(!active || isClosable(e.target))
|
|
|
|
) {
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
toggle();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleOutsideClick(e) {
|
2022-10-30 16:28:14 +08:00
|
|
|
if (active && !container?.contains(e.target) && !activeTrigger?.contains(e.target)) {
|
2022-07-07 05:19:05 +08:00
|
|
|
hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleEscPress(e) {
|
2022-10-30 16:28:14 +08:00
|
|
|
if (active && escClose && e.code === "Escape") {
|
2022-07-07 05:19:05 +08:00
|
|
|
e.preventDefault();
|
|
|
|
hide();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleFocusChange(e) {
|
|
|
|
return handleOutsideClick(e);
|
|
|
|
}
|
|
|
|
|
2022-10-30 16:28:14 +08:00
|
|
|
function bindTrigger(newTrigger) {
|
|
|
|
cleanup();
|
|
|
|
|
|
|
|
activeTrigger = newTrigger || container?.parentNode;
|
|
|
|
|
|
|
|
if (!activeTrigger) {
|
|
|
|
return;
|
|
|
|
}
|
2022-07-07 05:19:05 +08:00
|
|
|
|
2022-10-30 16:28:14 +08:00
|
|
|
container?.addEventListener("click", handleClickToggle);
|
|
|
|
activeTrigger.addEventListener("click", handleClickToggle);
|
|
|
|
activeTrigger.addEventListener("keydown", handleKeydownToggle);
|
|
|
|
}
|
|
|
|
|
|
|
|
function cleanup() {
|
|
|
|
if (!activeTrigger) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
container?.removeEventListener("click", handleClickToggle);
|
|
|
|
activeTrigger.removeEventListener("click", handleClickToggle);
|
|
|
|
activeTrigger.removeEventListener("keydown", handleKeydownToggle);
|
|
|
|
}
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
|
|
<svelte:window on:click={handleOutsideClick} on:keydown={handleEscPress} on:focusin={handleFocusChange} />
|
|
|
|
|
|
|
|
<div bind:this={container} class="toggler-container">
|
|
|
|
{#if active}
|
|
|
|
<div
|
|
|
|
class={classes}
|
|
|
|
class:active
|
|
|
|
in:fly|local={{ duration: 150, y: -5 }}
|
|
|
|
out:fly|local={{ duration: 150, y: 2 }}
|
|
|
|
>
|
|
|
|
<slot />
|
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
</div>
|