dialog-lite
Version:
DialogLite is designed to control a dialog box (modal window) on a web page, providing the functionality to open, close and apply custom styles through a simple interface.
270 lines (255 loc) • 9.96 kB
JavaScript
const s = `:root {
--z-index-dialog-lite: 992;
--z-index-dialog-lite-backdrop: 993;
--z-index-dialog-lite-container: 994;
}
.dialog-lite {
position: fixed;
inset: 0;
z-index: var(--z-index-dialog-lite, 992);
width: 100vw;
overflow: clip auto;
}
.dialog-lite--in {
-webkit-overflow-scrolling: touch;
}
.dialog-lite--out {
pointer-events: none;
}
.dialog-lite__backdrop {
position: fixed;
inset: 0;
margin: auto;
z-index: var(--z-index-dialog-lite-backdrop, 993);
}
.dialog-lite--in .dialog-lite__backdrop {
background-color: var(--c-dialog-lite-backdrop-in, hsla(240deg 22% 6% / 82%));
transition: background-color 400ms cubic-bezier(0.61, 1, 0.88, 1);
}
.dialog-lite--out .dialog-lite__backdrop {
pointer-events: none;
background-color: var(--c-dialog-lite-backdrop-out, hsla(200deg 2% 6% / 0%));
transition: background-color 500ms cubic-bezier(0, 0, 0.5, 1);
}
.dialog-lite__container {
pointer-events: none;
position: relative;
z-index: var(--z-index-dialog-lite-container, 994);
display: grid;
place-content: center;
width: 100vw;
}
@supports (min-height: 100dvh) {
.dialog-lite__container {
min-height: 100dvh;
}
}
@supports not (min-height: 100dvh) {
.dialog-lite__container {
min-height: 100vh;
}
}
.dialog-lite__container-inner {
position: relative;
margin: 20px;
}
.dialog-lite--in .dialog-lite__container-inner {
pointer-events: auto;
opacity: 1;
transform: translateY(0);
transition:
opacity 400ms cubic-bezier(0.61, 1, 0.88, 1),
transform 400ms cubic-bezier(0.61, 1, 0.88, 1);
}
.dialog-lite--out .dialog-lite__container-inner {
pointer-events: none;
opacity: 0;
transform: translateY(40px);
transition:
opacity 500ms cubic-bezier(0, 0, 0.5, 1),
transform 550ms cubic-bezier(0.22, 1, 0.5, 0.95);
}
.dialog-lite-close-button {
cursor: pointer;
position: absolute;
inset: 0 0 auto auto;
display: grid;
place-content: center;
width: 50px;
height: 50px;
}
.dialog-lite-close-button .svg-icon {
width: 24px;
height: 24px;
fill: black;
}
`, u = s;
function d(l, t) {
const e = typeof CSS < "u" && "escape" in CSS ? CSS.escape(t) : t.replace(/"/g, '\\"');
return l.querySelector(`#${e}`);
}
function c(l = {}) {
const { target: t = document, cssText: e = s, id: i = "dialog-lite-styles" } = l, n = d(t, i);
if (n && n instanceof HTMLStyleElement)
return n.textContent = e, n;
const o = document.createElement("style");
return o.id = i, o.textContent = e, t instanceof Document ? t.head.append(o) : t.append(o), o;
}
class r {
options;
dialogEl = null;
dialogCloseEl = null;
dialogBackdropEl = null;
mainContentEl = null;
currentExtraClass = "";
previouslyFocusedElement = null;
lastActionTime = 0;
isOpen = !1;
abortController = null;
hideTimeout = null;
removeExtraClassTimeout = null;
prevBodyOverflow = null;
prevBodyPaddingRight = null;
constructor(t = {}) {
this.options = {
closingButton: t.closingButton ?? !1,
closingBackdrop: t.closingBackdrop ?? !1,
dialog: t.dialog ?? ".dialog-lite",
mainContent: t.mainContent ?? "#main-content",
closeButtonSelector: t.closeButtonSelector ?? ".dialog-lite-close-button",
backdropSelector: t.backdropSelector ?? ".dialog-lite__backdrop",
debounceMs: t.debounceMs ?? 500,
hideDelayMs: t.hideDelayMs ?? 500,
focusOnOpenSelector: t.focusOnOpenSelector ?? '[tabindex="0"]',
lockScroll: t.lockScroll ?? !0,
trapFocus: t.trapFocus ?? !0,
role: t.role ?? "dialog",
ariaModal: t.ariaModal ?? !0,
emitEvents: t.emitEvents ?? !0
};
}
resolveHTMLElement(t) {
return t == null ? null : typeof t == "string" ? document.querySelector(t) : t;
}
resolveElementsOrThrow() {
if (this.dialogEl = this.resolveHTMLElement(this.options.dialog), this.mainContentEl = this.resolveHTMLElement(this.options.mainContent ?? null), !this.dialogEl)
throw new Error(
"Dialog element not found. Provide { dialog } option or ensure `.dialog-lite` exists."
);
this.dialogCloseEl = this.dialogEl.querySelector(this.options.closeButtonSelector), this.dialogBackdropEl = this.dialogEl.querySelector(this.options.backdropSelector), this.dialogEl.hasAttribute("tabindex") || this.dialogEl.setAttribute("tabindex", "-1"), this.options.role && this.dialogEl.setAttribute("role", this.options.role), this.options.ariaModal && this.dialogEl.setAttribute("aria-modal", "true");
}
emit(t, e) {
!this.options.emitEvents || !this.dialogEl || this.dialogEl.dispatchEvent(new CustomEvent(t, { detail: e }));
}
lockScroll() {
if (!this.options.lockScroll || this.prevBodyOverflow != null) return;
const t = document.body;
this.prevBodyOverflow = t.style.overflow, this.prevBodyPaddingRight = t.style.paddingRight;
const e = window.innerWidth - document.documentElement.clientWidth;
if (e > 0) {
const i = Number.parseFloat(getComputedStyle(t).paddingRight || "0");
t.style.paddingRight = `${i + e}px`;
}
t.style.overflow = "hidden";
}
unlockScroll() {
if (this.prevBodyOverflow == null) return;
const t = document.body;
t.style.overflow = this.prevBodyOverflow, t.style.paddingRight = this.prevBodyPaddingRight ?? "", this.prevBodyOverflow = null, this.prevBodyPaddingRight = null;
}
getFocusableElements() {
return this.dialogEl ? Array.from(this.dialogEl.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])')).filter(
(i) => !i.hasAttribute("disabled") && i.tabIndex >= 0 && i.getClientRects().length > 0
) : [];
}
handleFocusTrapKeydown(t) {
if (!this.options.trapFocus || !this.isOpen || t.key !== "Tab") return;
const e = this.getFocusableElements();
if (!e.length) {
this.dialogEl?.focus?.(), t.preventDefault();
return;
}
const i = document.activeElement, n = i instanceof HTMLElement ? e.indexOf(i) : -1, a = t.shiftKey ? n <= 0 ? e.length - 1 : n - 1 : n >= e.length - 1 ? 0 : n + 1;
e[a]?.focus?.(), t.preventDefault();
}
clearTimers() {
this.hideTimeout != null && (window.clearTimeout(this.hideTimeout), this.hideTimeout = null), this.removeExtraClassTimeout != null && (window.clearTimeout(this.removeExtraClassTimeout), this.removeExtraClassTimeout = null);
}
ensureInitialized() {
this.abortController || this.init();
}
init() {
this.destroy(), this.resolveElementsOrThrow(), this.abortController = new AbortController();
const t = this.abortController.signal;
this.options.closingButton && this.dialogCloseEl && this.dialogCloseEl.addEventListener("click", () => this.close(), { signal: t }), this.options.closingBackdrop && this.dialogBackdropEl && this.dialogBackdropEl.addEventListener("click", () => this.close(), { signal: t }), document.addEventListener(
"keydown",
(e) => {
e.key === "Escape" && this.isOpen && this.close();
},
{ signal: t }
), this.dialogEl?.addEventListener("keydown", (e) => this.handleFocusTrapKeydown(e), { signal: t });
}
destroy() {
this.abortController?.abort(), this.abortController = null, this.clearTimers(), this.unlockScroll();
}
open({ stylingClass: t = "" } = {}) {
if (this.isDebounced() || (this.ensureInitialized(), !this.dialogEl)) return;
this.clearTimers(), this.dialogEl.hidden = !1, this.dialogEl.offsetWidth, this.isOpen = !0, this.lockScroll(), this.mainContentEl && this.mainContentEl.setAttribute("aria-hidden", "true"), this.dialogEl.setAttribute("aria-hidden", "false");
const e = document.activeElement;
this.previouslyFocusedElement = e instanceof HTMLElement ? e : null, this.updateClassList({
addClass: "dialog-lite--in",
removeClass: "dialog-lite--out",
newClass: t
});
const i = this.dialogEl.querySelector(this.options.focusOnOpenSelector);
i?.focus?.(), i || this.dialogEl.focus(), this.emit("dialog-lite:open", { stylingClass: t });
}
close() {
this.isDebounced() || (this.ensureInitialized(), this.dialogEl && (this.isOpen = !1, this.unlockScroll(), this.mainContentEl && this.mainContentEl.setAttribute("aria-hidden", "false"), this.dialogEl.setAttribute("aria-hidden", "true"), this.previouslyFocusedElement?.isConnected && this.previouslyFocusedElement.focus(), this.updateClassList({
addClass: "dialog-lite--out",
removeClass: "dialog-lite--in",
newClass: "",
delayRemove: !0
}), this.hideTimeout = window.setTimeout(() => {
this.dialogEl && (this.dialogEl.hidden = !0), this.hideTimeout = null;
}, this.options.hideDelayMs), this.emit("dialog-lite:close", {})));
}
updateClassList({
addClass: t,
removeClass: e,
newClass: i,
delayRemove: n = !1
}) {
if (this.dialogEl) {
if (this.currentExtraClass)
if (n) {
const o = this.currentExtraClass;
this.removeExtraClassTimeout = window.setTimeout(() => {
this.dialogEl?.classList.remove(o), this.currentExtraClass = "", this.removeExtraClassTimeout = null;
}, this.options.hideDelayMs);
} else
this.dialogEl.classList.remove(this.currentExtraClass), this.currentExtraClass = "";
this.dialogEl.classList.remove(e), this.dialogEl.classList.add(t), i && (this.dialogEl.classList.add(i), this.currentExtraClass = i);
}
}
isDebounced() {
const t = Date.now();
return t - this.lastActionTime < this.options.debounceMs ? !0 : (this.lastActionTime = t, !1);
}
}
function h(l = {}) {
return new r(l);
}
function g(l = {}) {
const { injectCss: t = !0, cssText: e, cssTarget: i, ...n } = l;
t && c({ cssText: e, target: i });
const o = new r(n);
return o.init(), o;
}
export {
r as DialogLite,
h as createDialogLite,
u as dialogLiteCssText,
g as initDialogLite,
c as injectDialogLiteCss
};