UNPKG

@flexilla/offcanvas

Version:

An offcanvas component for creating responsive and off-screen navigation panels in web applications.

309 lines (308 loc) 11.9 kB
var g = Object.defineProperty; var w = (t, e, n) => e in t ? g(t, e, { enumerable: !0, configurable: !0, writable: !0, value: n }) : t[e] = n; var o = (t, e, n) => w(t, typeof e != "symbol" ? e + "" : e, n); const m = (t, e = document.body) => e.querySelector(t), v = (t, e = document.body) => Array.from(e.querySelectorAll(t)), y = ({ newElement: t, existingElement: e }) => { if (!(t instanceof HTMLElement) || !(e instanceof HTMLElement)) throw new Error("Both parameters must be valid HTML elements."); const n = e.parentElement; if (n) n.insertBefore(t, e); else throw new Error("Existing element must have a parent element."); }, k = ({ element: t, callback: e, type: n, keysCheck: s }) => { const a = getComputedStyle(t), i = a.transition; if (i !== "none" && i !== "" && !s.includes(i)) { const l = "transitionend", r = () => { t.removeEventListener(l, r), e(); }; t.addEventListener(l, r, { once: !0 }); } else e(); }, O = ({ element: t, callback: e }) => { k({ element: t, callback: e, type: "transition", keysCheck: ["all 0s ease 0s", "all"] }); }, h = (t, e, n) => { const s = new CustomEvent(e, { detail: n }); t.dispatchEvent(s); }; function A(t) { const e = () => { document.querySelector( "[data-fx-component]:not([data-component-initialized])" ) ? requestAnimationFrame(e) : t(); }; e(); } function B(t, e, n = "move") { if (!(t instanceof HTMLElement)) throw new Error("Source element must be an HTMLElement"); if (!(e instanceof HTMLElement)) throw new Error("Target element must be an HTMLElement"); if (!["move", "detachable"].includes(n)) throw new Error(`Invalid teleport mode: ${n}. Must be "move" or "detachable".`); let s = document.createComment("teleporter-placeholder"); const a = t.parentNode; return a ? a.insertBefore(s, t) : console.warn("Element has no parent; placeholder not inserted."), n === "move" ? (t.parentNode && e.appendChild(t), { append() { t.parentNode !== e && e.appendChild(t); }, remove() { s != null && s.parentNode && t.parentNode && s.parentNode.insertBefore(t, s); }, restore() { s != null && s.parentNode && t.parentNode !== a && s.parentNode.insertBefore(t, s); } }) : (t.parentNode && e.appendChild(t), { append() { e.contains(t) || e.appendChild(t); }, remove() { t.parentNode && t.remove(); }, restore() { s != null && s.parentNode && !t.parentNode && s.parentNode.insertBefore(t, s); } }); } const S = (t) => { var e; return (e = t.parentElement) == null ? void 0 : e.removeChild(t); }, C = (t) => { t.setAttribute("data-state", "invisible"), O({ element: t, callback() { S(t); } }); }, x = (t, e) => { const n = t; if (n === "" || !n) return; const s = document.createElement("div"); if (s.setAttribute("aria-hidden", "true"), s.setAttribute("data-state", "visible"), s.setAttribute("data-fx-offcanvas-overlay", ""), s.setAttribute("data-offcanvas-el", e), n === "") return; const a = n.split(" "); return n !== "" && s.classList.add(...a), s; }, u = (t, e, n) => { t.setAttribute("aria-hidden", n === "open" ? "false" : "true"), t.setAttribute("data-state", n), e || L(n); }, L = (t) => { document.body.style.overflow = t === "open" ? "hidden" : "", document.body.style.overflowY = t === "open" ? "hidden" : "auto"; }, I = (t, e) => { if (t === e) return; t.setAttribute("aria-hidden", "true"), t.setAttribute("data-state", "close"); const n = m(`[data-fx-offcanvas-overlay][data-offcanvas-el=${t.getAttribute("id")}]`, t.parentElement); n instanceof HTMLElement && C(n); }, T = (t) => { const e = v("[data-fx-offcanvas][data-state=open]"); if (!(e.length <= 0)) for (const n of e) I(n, t); }; class d { static initGlobalRegistry() { window.$flexillaInstances || (window.$flexillaInstances = {}); } static register(e, n, s) { return this.initGlobalRegistry(), window.$flexillaInstances[e] || (window.$flexillaInstances[e] = []), this.getInstance(e, n) || (window.$flexillaInstances[e].push({ element: n, instance: s }), s); } static getInstance(e, n) { var s, a; return this.initGlobalRegistry(), (a = (s = window.$flexillaInstances[e]) == null ? void 0 : s.find( (i) => i.element === n )) == null ? void 0 : a.instance; } static removeInstance(e, n) { this.initGlobalRegistry(), window.$flexillaInstances[e] && (window.$flexillaInstances[e] = window.$flexillaInstances[e].filter( (s) => s.element !== n )); } static setup(e) { e.setAttribute("data-fx-component", "fx"); } static initialized(e) { e.setAttribute("data-component-initialized", "initialized"); } } const f = class f { /** * Creates an instance of Offcanvas. * @param offcanvas - The offcanvas element selector or HTMLElement * @param options - Configuration options for the offcanvas * @throws {Error} When the provided element is not a valid HTMLElement * * @example * ```ts * const offcanvas = new Offcanvas('#sidebar', { * allowBodyScroll: true, // Allow scrolling when offcanvas is open * staticBackdrop: false, // Close when clicking outside * backdrop: 'dark', // Backdrop appearance * onShow: () => console.log('Offcanvas shown'), * onHide: () => console.log('Offcanvas hidden') * }); * ``` */ constructor(e, n = {}) { o(this, "offCanvasElement"); o(this, "offCanvasTriggers"); o(this, "offCanvasCloseBtns"); o(this, "allowBodyScroll"); o(this, "staticBackdrop"); o(this, "backdrop"); o(this, "options"); o(this, "teleporter"); o(this, "moveElOnInit", () => { A(() => this.teleporter.append()); }); o(this, "closeWhenClickOutSide", (e) => { const n = this.offCanvasElement.getAttribute("data-state") === "open", s = !this.offCanvasElement.contains(e.target) && ![...this.offCanvasTriggers].includes(e.target); n && s && this.closeOffCanvas(); }); o(this, "closeOffCanvas", () => { var i, l, r, c, p; let e = !1; if (h(this.offCanvasElement, "offcanvas-before-hide", { offcanvasId: this.offCanvasElement.id, setExitAction: (b) => { e = b; } }), ((r = (l = (i = this.options).beforeHide) == null ? void 0 : l.call(i)) == null ? void 0 : r.cancelAction) || e) return; const s = this.offCanvasElement.getAttribute("id"), a = m(`[data-fx-offcanvas-overlay][data-offcanvas-el=${s}]`); a instanceof HTMLElement && C(a), this.offCanvasElement.blur(), u( this.offCanvasElement, this.allowBodyScroll, "close" ), document.removeEventListener("keydown", this.closeWithEsc), !this.allowBodyScroll && !a && document.removeEventListener("click", this.closeWhenClickOutSide), (p = (c = this.options).onHide) == null || p.call(c), h(this.offCanvasElement, "offcanvas-close", { offcanvasId: this.offCanvasElement.id }); }); o(this, "closeWithEsc", (e) => { e.key === "Escape" && (e.preventDefault(), this.closeOffCanvas()); }); o(this, "changeState", () => { this.offCanvasElement.getAttribute("data-state") === "open" ? this.closeOffCanvas() : this.openOffCanvas(); }); o(this, "setOptions", ({ allowBodyscroll: e }) => { e !== void 0 && (this.allowBodyScroll = e); }); const s = typeof e == "string" ? m(e) : e; if (!(s instanceof HTMLElement)) throw new Error("Invalid Offcanvas, the provided Element is not a valid HTMLElement"); this.offCanvasElement = s; const a = d.getInstance("offcanvas", s); if (a) return a; d.setup(this.offCanvasElement), this.options = n; const { staticBackdrop: i, allowBodyScroll: l, backdrop: r } = this.options; this.setupAttributes(), this.staticBackdrop = i || s.hasAttribute("data-static-backdrop") && s.dataset.staticBackdrop !== "false" || !1, this.allowBodyScroll = l || s.hasAttribute("data-allow-body-scroll") && s.dataset.allowBodyScroll !== "false" || !1; const c = this.offCanvasElement.getAttribute("id"); this.offCanvasTriggers = this.findOffCanvasElements("[data-offcanvas-trigger]", !1, c), this.offCanvasCloseBtns = this.findOffCanvasElements("[data-offcanvas-close]", !0, c, this.offCanvasElement), this.backdrop = r || this.offCanvasElement.dataset.offcanvasBackdrop || "", this.teleporter = B(this.offCanvasElement, document.body, "move"), this.setupOffcanvas(), this.moveElOnInit(), d.register("offcanvas", this.offCanvasElement, this), d.initialized(this.offCanvasElement); } findOffCanvasElements(e, n, s, a) { return n ? v(`${e}`, a) : v(`${e}[data-target=${s}]`); } setupAttributes() { this.offCanvasElement.hasAttribute("data-fx-offcanvas") || this.offCanvasElement.setAttribute("data-fx-offcanvas", ""); } openOffCanvas() { var s, a, i, l; (a = (s = this.options).beforeShow) == null || a.call(s), T(this.offCanvasElement), u( this.offCanvasElement, this.allowBodyScroll, "open" ); const e = this.offCanvasElement.getAttribute("id"), n = x( this.backdrop, e ); n instanceof HTMLElement && (y({ newElement: n, existingElement: this.offCanvasElement }), this.staticBackdrop || n.addEventListener("click", this.closeOffCanvas)), document.addEventListener("keydown", this.closeWithEsc), (l = (i = this.options).onShow) == null || l.call(i), h(this.offCanvasElement, "offcanvas-open", { offcanvasId: this.offCanvasElement.id }); } initCloseBtns() { for (const e of this.offCanvasCloseBtns) e.addEventListener("click", this.closeOffCanvas); } initTriggers() { for (const e of this.offCanvasTriggers) e.addEventListener("click", this.changeState); } setupOffcanvas() { this.initTriggers(), this.initCloseBtns(); } /** * Opens the offcanvas element. * @example * ```ts * const offcanvas = new Offcanvas('#sidebar'); * offcanvas.open(); * ``` */ open() { this.openOffCanvas(); } /** * Closes the offcanvas element. * This method will trigger the beforeHide callback if provided, * remove the backdrop if present, and finally trigger the onHide callback. * * @example * ```ts * const offcanvas = new Offcanvas('#sidebar'); * offcanvas.close(); * ``` */ close() { this.closeOffCanvas(); } /** * Cleans up the offcanvas instance by removing event listeners and references. * Call this method when the offcanvas component is no longer needed to prevent memory leaks. * * @example * ```ts * const offcanvas = new Offcanvas('#sidebar'); * // ... use offcanvas ... * offcanvas.cleanup(); * ``` */ cleanup() { for (const e of this.offCanvasTriggers) e.removeEventListener("click", this.changeState); for (const e of this.offCanvasCloseBtns) e.removeEventListener("click", this.closeOffCanvas); document.removeEventListener("keydown", this.closeWithEsc), this.allowBodyScroll || document.removeEventListener("click", this.closeWhenClickOutSide), d.removeInstance("offcanvas", this.offCanvasElement); } }; /** * * @param selector - The selector for offcanvas elements to initialize. * @example * ```ts * Offcanvas.autoInit('#sidebar'); * ``` */ o(f, "autoInit", (e = "[data-fx-offcanvas]") => { const n = v(e); for (const s of n) new f(s); }), /** * This is an alternative to using the constructor directly. * @param offcanvas - The offcanvas element selector or HTMLElement * @param options - Configuration options for the offcanvas * @returns A new Offcanvas instance * * @example * ```ts * const offcanvas = Offcanvas.init('#sidebar', { * allowBodyScroll: true, * staticBackdrop: false * }); * ``` */ o(f, "init", (e, n = {}) => new f(e, n)); let E = f; export { E as OffCanvas };