UNPKG

@flexilla/dropdown

Version:

Utilities package for flexilla library

514 lines (513 loc) 23.7 kB
var j = Object.defineProperty; var q = (s, e, t) => e in s ? j(s, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : s[e] = t; var a = (s, e, t) => q(s, typeof e != "symbol" ? e + "" : e, t); var B = Object.defineProperty, N = (s, e, t) => e in s ? B(s, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : s[e] = t, d = (s, e, t) => N(s, typeof e != "symbol" ? e + "" : e, t); const V = "bottom", X = ({ reference: s, popper: e }) => { if (!s || !e) throw new Error("Reference or popper element is null or undefined"); const t = /* @__PURE__ */ new WeakMap(), n = (r) => (t.has(r) || t.set(r, r.getBoundingClientRect()), t.get(r)), o = n(e), i = n(s); return { popperHeight: o.height, popperWidth: o.width, refHeight: i.height, refWidth: i.width, refLeft: i.left, refTop: i.top, refRight: i.right }; }, Y = (s, e, t, n) => { const o = t, i = n - (t + e); return o >= (s - e) / 2 && i >= (s - e) / 2; }, J = (s, e, t, n) => (s - e) / 2 <= t && t + s / 2 + e / 2 <= n, Q = (s, e, t, n, o) => t > o - n ? e() ? window.innerHeight - o : t - o : s() ? 0 : t + n, Z = (s, e, t, n) => s <= n && t - s <= e, _ = (s, e, t, n) => t <= n && -s <= e, ee = (s, e, t, n, o, i) => { const r = o - t - i, h = t - n, g = t + i - n + (o - t - i), p = r >= 0 ? o - n : h >= 0 ? t - n : t; return s() ? 0 : e() ? g : p; }, te = (s, e, t, n) => s <= t && e - s - n >= s, ne = (s, e) => s >= e, se = ({ placement: s, refWidth: e, refTop: t, refLeft: n, refHeight: o, popperWidth: i, popperHeight: r, windowHeight: h, windowWidth: g, offsetDistance: p }) => { const c = g - n - e, f = n, v = h - t - o, m = t, w = () => Q( () => _(t, o, r, h), () => Z(t, o, r, h), t, o, r ), y = () => ee( () => te(n, g, i, e), () => ne(n, i), n, i, g, e ), H = () => Y(i, e, n, g) ? n + e / 2 - i / 2 : y(), T = () => J(r, o, t, h) ? t + o / 2 - r / 2 : w(), C = () => n + i <= g ? n : y(), O = () => n + e - i >= 0 ? n + e - i : y(), L = () => t + r <= h ? t : w(), G = () => t + o - r >= 0 ? t + o - r : w(); let u = 0, E = 0; const M = t - r - p, k = t + o + p, D = n - i - p, I = n + e + p, W = m >= r + p, F = v >= r + p, R = f >= i + p, $ = c >= i + p; switch (s.startsWith("top") ? E = W ? M : F ? k : Math.max(M, k) : s.startsWith("bottom") ? E = F ? k : W ? M : Math.max(k) : s.startsWith("left") ? u = R ? D : $ ? I : Math.max(D, I) : s.startsWith("right") && (u = $ ? I : R ? D : Math.max(I, D)), s) { case "bottom": case "bottom-middle": case "top": case "top-middle": u = H(); break; case "left": case "left-middle": case "right": case "right-middle": E = T(); break; case "bottom-start": case "top-start": u = C(); break; case "bottom-end": case "top-end": u = O(); break; case "left-start": case "right-start": E = L(); break; case "left-end": case "right-end": E = G(); break; } return { x: u, y: E }; }; class ie { /** * Flexilla Popper * @param reference * @param popper * @param options */ /** * Creates an instance of CreatePopper * @param {HTMLElement} reference - The reference element to position against * @param {HTMLElement} popper - The element to be positioned * @param {PopperOptions} [options] - Configuration options * @param {number} [options.offsetDistance] - Distance between popper and reference element * @param {Placement} [options.placement] - Preferred placement of the popper * @param {Object} [options.eventEffect] - Event handling configuration * @param {boolean} [options.eventEffect.disableOnResize] - Disable position updates on window resize * @param {boolean} [options.eventEffect.disableOnScroll] - Disable position updates on scroll * @param {Function} [options.onUpdate] - Callback function when position updates */ constructor(e, t, n = {}) { d(this, "reference"), d(this, "popper"), d(this, "offsetDistance"), d(this, "placement"), d(this, "disableOnResize"), d(this, "disableOnScroll"), d(this, "onUpdate"), d(this, "isWindowEventsRegistered"), d(this, "validateElements", () => { if (!(this.reference instanceof HTMLElement)) throw new Error("Invalid HTMLElement for Reference Element"); if (!(this.popper instanceof HTMLElement)) throw new Error("Invalid HTMLElement for Popper"); if (typeof this.offsetDistance != "number") throw new Error("OffsetDistance must be a number"); }), d(this, "setPopperStyleProperty", (c, f) => { this.popper.style.setProperty("--fx-popper-placement-x", `${c}px`), this.popper.style.setProperty("--fx-popper-placement-y", `${f}px`); }), d(this, "setInitialStyles", () => { this.popper.style.setProperty("--fx-popper-placement-x", ""), this.popper.style.setProperty("--fx-popper-placement-y", ""); }), d(this, "initPlacement", () => { var c; this.validateElements(), this.setInitialStyles(); const f = window.innerWidth, v = window.innerHeight, { popperHeight: m, popperWidth: w, refHeight: y, refWidth: H, refLeft: T, refTop: C } = X({ reference: this.reference, popper: this.popper }), { x: O, y: L } = se( { placement: this.placement, refWidth: H, refTop: C, refLeft: T, popperWidth: w, refHeight: y, popperHeight: m, windowHeight: v, windowWidth: f, offsetDistance: this.offsetDistance } ); this.setPopperStyleProperty(O, L), (c = this.onUpdate) == null || c.call(this, { x: O, y: L, placement: this.placement }); }), d(this, "removeWindowEvents", () => { this.isWindowEventsRegistered && (!this.disableOnResize && window.removeEventListener("resize", this.updatePosition), !this.disableOnScroll && window.removeEventListener("scroll", this.updatePosition), this.isWindowEventsRegistered = !1); }), d(this, "attachWindowEvent", () => { this.isWindowEventsRegistered && this.removeWindowEvents(), this.disableOnResize || window.addEventListener("resize", this.updatePosition), this.disableOnScroll || window.addEventListener("scroll", this.updatePosition), this.isWindowEventsRegistered = !0; }), d(this, "resetPosition", () => { this.setInitialStyles(); }), d(this, "updatePosition", () => { this.initPlacement(), this.attachWindowEvent(); }), d(this, "cleanupEvents", () => { this.setInitialStyles(), this.removeWindowEvents(); }); const { offsetDistance: o = 10, placement: i = V, eventEffect: r = {}, onUpdate: h } = n; if (!(e instanceof HTMLElement)) throw new Error("Invalid HTMLElement for Reference Element"); if (!(t instanceof HTMLElement)) throw new Error("Invalid HTMLElement for Popper"); if (n.offsetDistance && typeof n.offsetDistance != "number") throw new Error("OffsetDistance must be a number"); const { disableOnResize: g, disableOnScroll: p } = r; this.isWindowEventsRegistered = !1, this.reference = e, this.popper = t, this.offsetDistance = o, this.placement = i, this.disableOnResize = g || !1, this.disableOnScroll = p || !1, this.onUpdate = h; } /** * Updates popper configuration and recalculates position * @public * @param {Object} options - New configuration options * @param {Placement} options.placement - New placement value * @param {number} [options.offsetDistance] - New offset distance */ setOptions({ placement: e, offsetDistance: t }) { this.placement = e, this.offsetDistance = t || this.offsetDistance, this.initPlacement(), this.attachWindowEvent(); } } var oe = Object.defineProperty, re = (s, e, t) => e in s ? oe(s, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : s[e] = t, l = (s, e, t) => re(s, typeof e != "symbol" ? e + "" : e, t); const ae = (s, e = document.body) => e.querySelector(s), A = (s, e) => { for (const [t, n] of Object.entries(e)) s.setAttribute(t, n); }, le = ({ element: s, callback: e, type: t, keysCheck: n }) => { const o = getComputedStyle(s), i = o.transition; if (i !== "none" && i !== "" && !n.includes(i)) { const r = "transitionend", h = () => { s.removeEventListener(r, h), e(); }; s.addEventListener(r, h, { once: !0 }); } else e(); }, he = ({ element: s, callback: e }) => { le({ element: s, callback: e, type: "transition", keysCheck: ["all 0s ease 0s", "all"] }); }, b = ({ state: s, trigger: e, popper: t }) => { const n = s === "open"; A(t, { "data-state": s }), A(e, { "aria-expanded": `${n}` }); }; class ce { /** * Creates an instance of CreateOverlay * @param {Object} params - The initialization parameters * @param {string | HTMLElement} params.trigger - The trigger element selector or HTMLElement * @param {string | HTMLElement} params.content - The content element selector or HTMLElement * @param {OverlayOptions} [params.options] - Configuration options for the overlay */ constructor({ trigger: e, content: t, options: n = {} }) { l(this, "triggerElement"), l(this, "contentElement"), l(this, "triggerStrategy"), l(this, "placement"), l(this, "offsetDistance"), l(this, "preventFromCloseOutside"), l(this, "preventFromCloseInside"), l(this, "options"), l(this, "defaultState"), l(this, "popper"), l(this, "eventEffect"), l(this, "getElement", (i) => typeof i == "string" ? ae(i) : i instanceof HTMLElement ? i : void 0), l(this, "handleDocumentClick", (i) => { this.contentElement.getAttribute("data-state") === "open" && (!this.triggerElement.contains(i.target) && !this.preventFromCloseInside && !this.preventFromCloseOutside ? this.hide() : !this.triggerElement.contains(i.target) && !this.contentElement.contains(i.target) && !this.preventFromCloseOutside ? this.hide() : !this.triggerElement.contains(i.target) && !this.contentElement.contains(i.target) && !this.preventFromCloseOutside ? this.hide() : !this.triggerElement.contains(i.target) && this.contentElement.contains(i.target) && !this.preventFromCloseInside && this.hide()); }), l(this, "handleKeyDown", (i) => { i.preventDefault(), this.triggerStrategy !== "hover" && i.key === "Escape" && this.contentElement.getAttribute("data-state") === "open" && (this.preventFromCloseOutside || this.hide()); }), l(this, "toggleStateOnClick", () => { (this.contentElement.dataset.state || "close") === "close" ? (this.show(), this.triggerStrategy === "hover" && this.addEventOnMouseEnter()) : this.hide(); }), l(this, "hideOnMouseLeaseTrigger", () => { setTimeout(() => { this.contentElement.matches(":hover") || this.hide(); }, 150); }), l(this, "hideOnMouseLeave", () => { setTimeout(() => { this.triggerElement.matches(":hover") || this.hide(); }, 150); }), l(this, "addEventOnMouseEnter", () => { this.triggerElement.addEventListener("mouseleave", this.hideOnMouseLeaseTrigger), this.contentElement.addEventListener("mouseleave", this.hideOnMouseLeave); }), l(this, "showOnMouseEnter", () => { this.show(), this.addEventOnMouseEnter(); }), l(this, "setShowOptions", ({ placement: i, offsetDistance: r }) => { var h, g, p, c; this.popper.setOptions({ placement: i, offsetDistance: r }), document.addEventListener("keydown", this.handleKeyDown), document.addEventListener("click", this.handleDocumentClick), (g = (h = this.options).beforeShow) == null || g.call(h), b({ state: "open", popper: this.contentElement, trigger: this.triggerElement }), this.onToggleState(!1), (c = (p = this.options).onShow) == null || c.call(p); }), l(this, "setPopperOptions", ({ placement: i, offsetDistance: r }) => { this.popper.setOptions({ placement: i, offsetDistance: r || this.offsetDistance }); }), l(this, "setPopperTrigger", (i, r) => { this.cleanup(), this.popper.setOptions({ placement: r.placement || this.placement, offsetDistance: r.offsetDistance || this.offsetDistance }), this.triggerElement = i, this.triggerElement.addEventListener("click", this.toggleStateOnClick), this.triggerStrategy === "hover" && this.triggerElement.addEventListener("mouseenter", this.showOnMouseEnter); }), l(this, "cleanup", () => { this.triggerElement.removeEventListener("click", this.toggleStateOnClick), this.triggerStrategy === "hover" && this.triggerElement.removeEventListener("mouseenter", this.showOnMouseEnter); }); var o; if (this.contentElement = this.getElement(t), this.triggerElement = this.getElement(e), !(this.triggerElement instanceof HTMLElement)) throw new Error("Trigger element must be a valid HTML element"); if (!(this.contentElement instanceof HTMLElement)) throw new Error("Content element must be a valid HTML element"); this.options = n, this.triggerStrategy = this.options.triggerStrategy || "click", this.placement = this.options.placement || "bottom", this.offsetDistance = this.options.offsetDistance || 6, this.preventFromCloseOutside = this.options.preventFromCloseOutside || !1, this.preventFromCloseInside = this.options.preventCloseFromInside || !1, this.defaultState = this.options.defaultState || "close", this.eventEffect = (o = this.options.popper) == null ? void 0 : o.eventEffect, this.popper = new ie( this.triggerElement, this.contentElement, { placement: this.placement, offsetDistance: this.offsetDistance, eventEffect: this.eventEffect } ), this.initInstance(); } onToggleState(e) { var t, n; (n = (t = this.options).onToggle) == null || n.call(t, { isHidden: e }); } /** * Shows the overlay * Positions the overlay, adds event listeners, and triggers related callbacks */ show() { var e, t, n, o; this.popper.updatePosition(), document.addEventListener("keydown", this.handleKeyDown), document.addEventListener("click", this.handleDocumentClick), (t = (e = this.options).beforeShow) == null || t.call(e), b({ state: "open", popper: this.contentElement, trigger: this.triggerElement }), this.onToggleState(!1), (o = (n = this.options).onShow) == null || o.call(n); } /** * Hides the overlay * Removes event listeners and triggers related callbacks */ hide() { var e, t; (t = (e = this.options).beforeHide) == null || t.call(e), b({ state: "close", popper: this.contentElement, trigger: this.triggerElement }), this.triggerStrategy === "click" && document.removeEventListener("click", this.handleDocumentClick), document.removeEventListener("keydown", this.handleKeyDown), this.triggerStrategy === "hover" && (this.triggerElement.removeEventListener("mouseleave", this.hideOnMouseLeaseTrigger), this.contentElement.removeEventListener("mouseleave", this.hideOnMouseLeave)), he({ element: this.contentElement, callback: () => { var n, o; this.onToggleState(!0), this.popper.cleanupEvents(), (o = (n = this.options).onHide) == null || o.call(n); } }); } initInstance() { b({ state: this.defaultState, popper: this.contentElement, trigger: this.triggerElement }), this.defaultState === "open" ? this.show() : b({ state: "close", popper: this.contentElement, trigger: this.triggerElement }), this.triggerElement.addEventListener("click", this.toggleStateOnClick), this.triggerStrategy === "hover" && this.triggerElement.addEventListener("mouseenter", this.showOnMouseEnter); } } const P = (s, e = document.body) => e.querySelector(s), U = (s, e = document.body) => Array.from(e.querySelectorAll(s)), pe = (s) => typeof s == "string" ? P(s) : s, de = ({ containerElement: s, targetChildren: e = "a:not([disabled]), button:not([disabled])", direction: t }) => { let n = !1; const o = pe(s) || document.body, i = typeof e == "string" ? U(e, o) : e, r = (h) => { if (h.preventDefault(), o.focus(), i.length === 0) return; const g = h.key, p = document.activeElement; let c = i.findIndex((m) => m === p); if (c === -1) { g === "ArrowUp" || g === "ArrowLeft" ? i[i.length - 1].focus() : i[0].focus(); return; } const f = (m) => m > 0 ? m - 1 : i.length - 1, v = (m) => m < i.length - 1 ? m + 1 : 0; switch (g) { case "ArrowDown": h.preventDefault(), c = v(c); break; case "ArrowRight": break; case "ArrowUp": h.preventDefault(), c = f(c); break; case "ArrowLeft": break; case "Home": h.preventDefault(), c = 0; break; case "End": h.preventDefault(), c = i.length - 1; break; default: return; } i[c] !== p && i[c].focus(); }; return { make: () => { n || (document.addEventListener("keydown", r), n = !0); }, destroy: () => { n && (document.removeEventListener("keydown", r), n = !1); } }; }, z = (s, e, t) => { const n = new CustomEvent(e, { detail: t }); s.dispatchEvent(n); }; class x { static initGlobalRegistry() { window.$flexillaInstances || (window.$flexillaInstances = {}); } static register(e, t, n) { return this.initGlobalRegistry(), window.$flexillaInstances[e] || (window.$flexillaInstances[e] = []), this.getInstance(e, t) || (window.$flexillaInstances[e].push({ element: t, instance: n }), n); } static getInstance(e, t) { var n, o; return this.initGlobalRegistry(), (o = (n = window.$flexillaInstances[e]) == null ? void 0 : n.find( (i) => i.element === t )) == null ? void 0 : o.instance; } static removeInstance(e, t) { this.initGlobalRegistry(), window.$flexillaInstances[e] && (window.$flexillaInstances[e] = window.$flexillaInstances[e].filter( (n) => n.element !== t )); } } const S = class S { /** * Creates a new Dropdown instance * @param dropdown - The dropdown content element or selector * @param options - Configuration options for the dropdown * @throws {Error} If provided elements are not valid HTMLElements */ constructor(e, t = {}) { a(this, "triggerElement"); a(this, "contentElement"); a(this, "options"); a(this, "OverlayInstance"); a(this, "navigationKeys"); a(this, "triggerStrategy"); a(this, "placement"); a(this, "offsetDistance"); a(this, "preventFromCloseOutside"); a(this, "preventFromCloseInside"); a(this, "defaultState"); a(this, "onToggle", ({ isHidden: e }) => { var t, n; (n = (t = this.options).onToggle) == null || n.call(t, { isHidden: e }); }); a(this, "beforeShow", () => { this.contentElement.focus(), this.navigationKeys.make(); }); a(this, "beforeHide", () => { this.contentElement.blur(), this.navigationKeys.destroy(); }); a(this, "onShow", () => { var e, t; z(this.contentElement, "dropdown-show", { isHidden: !1 }), (t = (e = this.options).onShow) == null || t.call(e); }); a(this, "onHide", () => { var e, t; z(this.contentElement, "dropdown-hide", { isHidden: !0 }), (t = (e = this.options).onHide) == null || t.call(e); }); /** * Shows the dropdown */ a(this, "show", () => { this.OverlayInstance.show(); }); /** * Hides the dropdown */ a(this, "hide", () => { this.OverlayInstance.hide(); }); /** * Updates the dropdown's placement and offset settings and show it * @param options - The new placement and offset options */ a(this, "setShowOptions", ({ placement: e, offsetDistance: t }) => { this.OverlayInstance.setShowOptions({ placement: e, offsetDistance: t }); }); /** * Updates the dropdown's placement and offset settings * @param options - The new placement and offset options */ a(this, "setOptions", ({ placement: e, offsetDistance: t }) => { this.OverlayInstance.setPopperOptions({ placement: e, offsetDistance: t }); }); /** * Updates the dropdown trigger reference Element and options * The new set trigger will be used as reference for the Dropdown */ a(this, "setPopperTrigger", (e, t) => { this.OverlayInstance.setPopperTrigger(e, t); }); /** * Removes all event listeners */ a(this, "cleanup", () => { this.OverlayInstance.cleanup(), x.removeInstance("dropdown", this.contentElement); }); const n = typeof e == "string" ? P(e) : e; if (!(n instanceof HTMLElement)) throw new Error( "Invalid dropdown content element: Must provide either a valid HTMLElement or a selector string that resolves to an existing HTMLElement" ); if (!n.id) throw new Error("Dropdown content element must have an 'id' attribute for trigger association"); this.contentElement = n; const o = x.getInstance("dropdown", this.contentElement); if (o) return o; const i = `[data-dropdown-trigger][data-dropdown-id=${this.contentElement.id}]`; if (this.triggerElement = P(i), !(this.triggerElement instanceof HTMLElement)) throw new Error(`No valid trigger element found. Ensure a trigger element exists with attributes: data-dropdown-trigger and data-dropdown-id="${this.contentElement.id}"`); this.options = t, this.triggerStrategy = this.options.triggerStrategy || this.contentElement.dataset.triggerStrategy || "click", this.placement = this.options.placement || this.contentElement.dataset.placement || "bottom-start", this.offsetDistance = this.options.offsetDistance || parseInt(`${this.contentElement.dataset.offsetDistance}`) | 6, this.preventFromCloseOutside = this.options.preventFromCloseOutside || this.contentElement.hasAttribute("data-prevent-close-outside") || !1, this.preventFromCloseInside = this.options.preventCloseFromInside || this.contentElement.hasAttribute("data-prevent-close-inside") || !1, this.defaultState = this.options.defaultState || this.contentElement.dataset.defaultState || "close", this.OverlayInstance = new ce({ trigger: this.triggerElement, content: this.contentElement, options: { placement: this.placement, offsetDistance: this.offsetDistance, triggerStrategy: this.triggerStrategy, preventFromCloseOutside: this.preventFromCloseOutside, preventCloseFromInside: this.preventFromCloseInside, defaultState: this.defaultState, beforeShow: this.beforeShow, beforeHide: this.beforeHide, onShow: this.onShow, onHide: this.onHide, onToggle: ({ isHidden: r }) => { this.onToggle({ isHidden: r }); }, popper: this.options.popper } }), this.navigationKeys = de({ containerElement: this.contentElement, targetChildren: "a:not([disabled]), button:not([disabled])", direction: "up-down" }), x.register("dropdown", this.contentElement, this); } /** * Initializes a single dropdown instance * @param dropdown - The dropdown element or selector * @param options - Configuration options for the dropdown * @returns A new Dropdown instance */ static init(e, t = {}) { new S(e, t); } }; /** * Automatically initializes all dropdown elements that match the given selector * @param selector - The selector to find dropdown elements (default: "[data-fx-dropdown]") */ a(S, "autoInit", (e = "[data-fx-dropdown]") => { const t = U(e); for (const n of t) new S(n); }); let K = S; export { K as Dropdown };