UNPKG

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
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 };