238 lines
6.7 KiB
Svelte
238 lines
6.7 KiB
Svelte
|
<script context="module">
|
||
|
let holder;
|
||
|
|
||
|
function getHolder() {
|
||
|
holder = holder || document.querySelector(".overlays");
|
||
|
|
||
|
if (!holder) {
|
||
|
// create
|
||
|
holder = document.createElement("div");
|
||
|
holder.classList.add("overlays");
|
||
|
document.body.appendChild(holder);
|
||
|
}
|
||
|
|
||
|
return holder;
|
||
|
}
|
||
|
</script>
|
||
|
|
||
|
<script>
|
||
|
/**
|
||
|
* Example usage:
|
||
|
* ```html
|
||
|
* <OverlayPanel bind:active={popupActive} popup={false}>
|
||
|
* <h5 slot="header">My title</h5>
|
||
|
* <p>Lorem ipsum dolor sit amet...</p>
|
||
|
* <svelte:fragment slot="footer">
|
||
|
* <button class="btn btn-secondary">Cancel</button>
|
||
|
* <button class="btn btn-expanded">Save</button>
|
||
|
* </svelte:fragment>
|
||
|
* </OverlayPanel>
|
||
|
* ```
|
||
|
*/
|
||
|
import { onMount, createEventDispatcher, tick } from "svelte";
|
||
|
import { fade, fly } from "svelte/transition";
|
||
|
import CommonHelper from "@/utils/CommonHelper";
|
||
|
|
||
|
let classes = "";
|
||
|
export { classes as class }; // export reserved keyword
|
||
|
|
||
|
export let active = false;
|
||
|
export let popup = false;
|
||
|
export let overlayClose = true;
|
||
|
export let btnClose = true;
|
||
|
export let escClose = true;
|
||
|
export let beforeOpen = undefined; // function callback called before open; if return false - no open
|
||
|
export let beforeHide = undefined; // function callback called before hide; if return false - no close
|
||
|
|
||
|
const dispatch = createEventDispatcher();
|
||
|
|
||
|
let wrapper;
|
||
|
let contentPanel;
|
||
|
let oldFocusedElem;
|
||
|
let transitionSpeed = 150;
|
||
|
let contentScrollThrottle;
|
||
|
let contentScrollClass = "";
|
||
|
|
||
|
$: onActiveChange(active);
|
||
|
|
||
|
$: handleContentScroll(contentPanel, true);
|
||
|
|
||
|
$: if (wrapper) {
|
||
|
zIndexUpdate();
|
||
|
}
|
||
|
|
||
|
export function show() {
|
||
|
if (typeof beforeOpen === "function" && beforeOpen() === false) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
active = true;
|
||
|
}
|
||
|
|
||
|
export function hide() {
|
||
|
if (typeof beforeHide === "function" && beforeHide() === false) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
active = false;
|
||
|
}
|
||
|
|
||
|
export function isActive() {
|
||
|
return active;
|
||
|
}
|
||
|
|
||
|
async function onActiveChange(state) {
|
||
|
if (state) {
|
||
|
oldFocusedElem = document.activeElement;
|
||
|
wrapper?.focus();
|
||
|
dispatch("show");
|
||
|
} else {
|
||
|
clearTimeout(contentScrollThrottle);
|
||
|
oldFocusedElem?.focus();
|
||
|
dispatch("hide");
|
||
|
}
|
||
|
|
||
|
await tick();
|
||
|
|
||
|
zIndexUpdate();
|
||
|
}
|
||
|
|
||
|
function zIndexUpdate() {
|
||
|
if (!wrapper) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (active) {
|
||
|
wrapper.style.zIndex = highestZIndex();
|
||
|
} else {
|
||
|
wrapper.style = "";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function highestZIndex() {
|
||
|
return 1000 + getHolder().querySelectorAll(".overlay-panel-container.active").length;
|
||
|
}
|
||
|
|
||
|
function handleEscPress(e) {
|
||
|
if (
|
||
|
active &&
|
||
|
escClose &&
|
||
|
e.code == "Escape" &&
|
||
|
!CommonHelper.isInput(e.target) &&
|
||
|
wrapper &&
|
||
|
// it is the top most popup
|
||
|
wrapper.style.zIndex == highestZIndex()
|
||
|
) {
|
||
|
e.preventDefault();
|
||
|
hide();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function handleResize(e) {
|
||
|
if (active) {
|
||
|
handleContentScroll(contentPanel);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function handleContentScroll(panel, reset) {
|
||
|
if (reset) {
|
||
|
contentScrollClass = "";
|
||
|
}
|
||
|
|
||
|
if (!panel) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!contentScrollThrottle) {
|
||
|
contentScrollThrottle = setTimeout(() => {
|
||
|
clearTimeout(contentScrollThrottle);
|
||
|
contentScrollThrottle = null;
|
||
|
|
||
|
if (!panel) {
|
||
|
return; // deleted during timeout
|
||
|
}
|
||
|
|
||
|
let heightDiff = panel.scrollHeight - panel.offsetHeight;
|
||
|
if (heightDiff > 0) {
|
||
|
contentScrollClass = "scrollable";
|
||
|
} else {
|
||
|
contentScrollClass = "";
|
||
|
return; // no scroll
|
||
|
}
|
||
|
|
||
|
if (panel.scrollTop == 0) {
|
||
|
contentScrollClass += " scroll-top-reached";
|
||
|
} else if (panel.scrollTop + panel.offsetHeight == panel.scrollHeight) {
|
||
|
contentScrollClass += " scroll-bottom-reached";
|
||
|
}
|
||
|
}, 100);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
onMount(() => {
|
||
|
// move outside of its current parent
|
||
|
getHolder().appendChild(wrapper);
|
||
|
|
||
|
return () => {
|
||
|
clearTimeout(contentScrollThrottle);
|
||
|
|
||
|
// ensures that no artifacts remains
|
||
|
// (currently there is a bug with svelte transition)
|
||
|
wrapper?.classList?.add("hidden");
|
||
|
};
|
||
|
});
|
||
|
</script>
|
||
|
|
||
|
<svelte:window on:resize={handleResize} on:keydown={handleEscPress} />
|
||
|
|
||
|
<div class="overlay-panel-wrapper" bind:this={wrapper}>
|
||
|
{#if active}
|
||
|
<div class="overlay-panel-container" class:padded={popup} class:active>
|
||
|
<div
|
||
|
class="overlay"
|
||
|
on:click|preventDefault={() => (overlayClose ? hide() : true)}
|
||
|
transition:fade={{ duration: transitionSpeed, opacity: 0 }}
|
||
|
/>
|
||
|
|
||
|
<div
|
||
|
class="overlay-panel {classes} {contentScrollClass}"
|
||
|
class:popup
|
||
|
in:fly={popup ? { duration: transitionSpeed, y: -10 } : { duration: transitionSpeed, x: 50 }}
|
||
|
out:fly={popup ? { duration: transitionSpeed, y: 10 } : { duration: transitionSpeed, x: 50 }}
|
||
|
>
|
||
|
<div class="overlay-panel-section panel-header">
|
||
|
{#if btnClose && !popup}
|
||
|
<div class="overlay-close" on:click|preventDefault={hide}>
|
||
|
<i class="ri-close-line" />
|
||
|
</div>
|
||
|
{/if}
|
||
|
|
||
|
<slot name="header" />
|
||
|
|
||
|
{#if btnClose && popup}
|
||
|
<button
|
||
|
type="button"
|
||
|
class="btn btn-sm btn-circle btn-secondary btn-close m-l-auto"
|
||
|
on:click|preventDefault={hide}
|
||
|
>
|
||
|
<i class="ri-close-line txt-lg" />
|
||
|
</button>
|
||
|
{/if}
|
||
|
</div>
|
||
|
|
||
|
<div
|
||
|
bind:this={contentPanel}
|
||
|
class="overlay-panel-section panel-content"
|
||
|
on:scroll={(e) => handleContentScroll(e.target)}
|
||
|
>
|
||
|
<slot />
|
||
|
</div>
|
||
|
|
||
|
<div class="overlay-panel-section panel-footer">
|
||
|
<slot name="footer" />
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
{/if}
|
||
|
</div>
|