UNPKG

@pageblock/utils

Version:

A modern utility library for PageBlock and Webflow, providing reusable components and utilities for web applications and Webflow sites

343 lines (342 loc) 12.1 kB
class Modal { constructor(options = {}) { this.options = { debug: options.debug || false, ...options }; this.modalState = /* @__PURE__ */ new WeakMap(); this.zIndexCounter = 1e3; this.activeModals = []; this.initialize(); } /** * Debug logging function * @private */ log(...args) { if (this.options.debug) { console.log("[Modal]", ...args); } } initialize() { const allModalContainers = document.querySelectorAll('pageblock-modal[data-modal-id], [data-pb-modal="container"][data-modal-id]'); allModalContainers.forEach((container) => { this.initializeModalContainer(container); }); const legacySheets = document.querySelectorAll('[data-pb-modal="sheet"][data-modal-id]'); legacySheets.forEach((sheet) => { if (!sheet.closest("pageblock-modal")) { this.initializeLegacyModal(sheet); } }); document.addEventListener("click", this.handleDocumentClick.bind(this)); document.addEventListener("keydown", this.handleKeyDown.bind(this)); document.addEventListener("pb:modal:open", this.handleCustomOpenEvent.bind(this)); document.addEventListener("pb:modal:close", this.handleCustomCloseEvent.bind(this)); this.log("Modal system initialized"); } initializeModalContainer(container) { const modalId = container.getAttribute("data-modal-id"); if (!modalId) { console.warn("Modal container found without data-modal-id attribute", container); return; } const modal = container.querySelector('[data-pb-modal="sheet"]'); const overlay = container.querySelector('[data-pb-modal="overlay"]'); if (!modal || !overlay) { console.warn(`Modal components missing in container with ID: ${modalId}`, container); return; } modal.setAttribute("role", "dialog"); modal.setAttribute("aria-modal", "true"); modal.setAttribute("aria-hidden", "true"); const animation = container.getAttribute("data-animation"); const variant = container.getAttribute("data-variant"); this.modalState.set(container, { id: modalId, isOpen: false, previousFocus: null, animation: animation || "fade", variant: variant || "default", zIndex: parseInt(modal.style.zIndex) || this.zIndexCounter, modal, overlay, focusableElements: this.getFocusableElements(modal) }); this.log(`Initialized modal: ${modalId}`); } initializeLegacyModal(sheet) { const modalId = sheet.getAttribute("data-modal-id"); if (!modalId) return; let overlay = document.querySelector('[data-pb-modal="overlay"]'); if (!overlay) { overlay = document.createElement("div"); overlay.setAttribute("data-pb-modal", "overlay"); document.body.appendChild(overlay); } sheet.setAttribute("role", "dialog"); sheet.setAttribute("aria-modal", "true"); sheet.setAttribute("aria-hidden", "true"); const virtualContainer = { getAttribute: (attr) => attr === "data-modal-id" ? modalId : null, dispatchEvent: (event) => sheet.dispatchEvent(event) }; this.modalState.set(virtualContainer, { id: modalId, isOpen: false, previousFocus: null, animation: "fade", variant: "default", zIndex: parseInt(sheet.style.zIndex) || this.zIndexCounter, modal: sheet, overlay, focusableElements: this.getFocusableElements(sheet), isLegacy: true }); this.log(`Initialized legacy modal: ${modalId}`); } handleDocumentClick(event) { const trigger = event.target.closest("[data-pb-modal-trigger]"); if (trigger) { event.preventDefault(); const modalId = trigger.getAttribute("data-pb-modal-trigger"); this.openModal(modalId, { trigger }); } const closeButton = event.target.closest('[data-pb-modal="close"]'); if (closeButton) { event.preventDefault(); const container = closeButton.closest('pageblock-modal, [data-pb-modal="container"]'); const modalId = container == null ? void 0 : container.getAttribute("data-modal-id"); const sheet = closeButton.closest('[data-pb-modal="sheet"]'); const legacyModalId = sheet == null ? void 0 : sheet.getAttribute("data-modal-id"); this.closeModal(modalId || legacyModalId); } const overlay = event.target.closest('[data-pb-modal="overlay"]'); if (overlay && overlay === event.target) { event.preventDefault(); if (this.activeModals.length > 0) { const topModalId = this.activeModals[this.activeModals.length - 1]; this.closeModal(topModalId); } } } handleKeyDown(event) { if (event.key === "Escape" && this.activeModals.length > 0) { const topModalId = this.activeModals[this.activeModals.length - 1]; this.closeModal(topModalId); } else if (event.key === "Tab" && this.activeModals.length > 0) { const topModalId = this.activeModals[this.activeModals.length - 1]; const container = this.findModalContainer(topModalId); if (container) { const state = this.modalState.get(container); if (state && state.modal) { this.trapFocus(event, state.modal); } } } } handleCustomOpenEvent(event) { const modalId = event.detail.modalId; const options = event.detail.options || {}; this.openModal(modalId, options); } handleCustomCloseEvent(event) { const modalId = event.detail.modalId; const options = event.detail.options || {}; this.closeModal(modalId, options); } findModalContainer(modalId) { let container = document.querySelector(`pageblock-modal[data-modal-id="${modalId}"], [data-pb-modal="container"][data-modal-id="${modalId}"]`); if (!container) { const legacySheet = document.querySelector(`[data-pb-modal="sheet"][data-modal-id="${modalId}"]`); if (legacySheet) { container = { getAttribute: (attr) => attr === "data-modal-id" ? modalId : null, dispatchEvent: (event) => legacySheet.dispatchEvent(event) }; } } return container; } openModal(modalId, options = {}) { if (!modalId) { console.error("Modal ID is required to open a modal"); return; } const container = this.findModalContainer(modalId); if (!container) { console.error(`Modal container not found for ID: ${modalId}`); return; } const state = this.modalState.get(container); if (!state) { console.error(`Modal state not found for ID: ${modalId}`); return; } const modal = state.modal; const overlay = state.overlay; if (state.isOpen) return; state.previousFocus = document.activeElement; this.zIndexCounter += 2; state.zIndex = this.zIndexCounter; modal.style.zIndex = state.zIndex; overlay.style.zIndex = state.zIndex - 1; modal.setAttribute("aria-hidden", "false"); modal.classList.add("cc-active"); overlay.classList.add("cc-active"); if (options.animation && !state.isLegacy) { container.dataset.animation = options.animation; } if (options.variant && !state.isLegacy) { container.dataset.variant = options.variant; } document.body.style.overflow = "hidden"; this.activeModals.push(modalId); state.isOpen = true; this.modalState.set(container, state); setTimeout(() => { const focusTarget = modal.querySelector("[data-pb-modal-autofocus]") || state.focusableElements[0] || modal; if (focusTarget) { focusTarget.focus(); } }, 50); container.dispatchEvent(new CustomEvent("modal:opened", { detail: { modalId, options } })); if (typeof options.onOpen === "function") { options.onOpen(modal, overlay, container); } this.log(`Opened modal: ${modalId}`); } closeModal(modalId, options = {}) { if (!modalId) { console.error("Modal ID is required to close a modal"); return; } const container = this.findModalContainer(modalId); if (!container) { console.error(`Modal container not found for ID: ${modalId}`); return; } const state = this.modalState.get(container); if (!state || !state.isOpen) return; const modal = state.modal; const overlay = state.overlay; modal.setAttribute("aria-hidden", "true"); modal.classList.remove("cc-active"); let otherModalsUsingOverlay = false; for (let i = 0; i < this.activeModals.length; i++) { const activeId = this.activeModals[i]; if (activeId !== modalId) { const activeContainer = this.findModalContainer(activeId); if (activeContainer) { const activeState = this.modalState.get(activeContainer); if (activeState && activeState.overlay === overlay) { otherModalsUsingOverlay = true; break; } } } } if (!otherModalsUsingOverlay) { overlay.classList.remove("cc-active"); } const index = this.activeModals.indexOf(modalId); if (index > -1) { this.activeModals.splice(index, 1); } if (this.activeModals.length === 0) { document.body.style.overflow = ""; } state.isOpen = false; this.modalState.set(container, state); const transitionEndHandler = () => { if (state.previousFocus && state.previousFocus.focus) { state.previousFocus.focus(); } container.dispatchEvent(new CustomEvent("modal:closed", { detail: { modalId, options } })); if (typeof options.onClose === "function") { options.onClose(modal, overlay, container); } modal.removeEventListener("transitionend", transitionEndHandler); }; modal.addEventListener("transitionend", transitionEndHandler); this.log(`Closed modal: ${modalId}`); } closeAllModals() { const modalsToClose = [...this.activeModals]; modalsToClose.forEach((modalId) => { this.closeModal(modalId); }); this.log("Closed all modals"); } // Utility function to get all focusable elements within a container getFocusableElements(container) { const focusableSelectors = [ "a[href]:not([disabled])", "button:not([disabled])", "textarea:not([disabled])", "input:not([disabled])", "select:not([disabled])", '[tabindex]:not([tabindex="-1"])' ]; return Array.from(container.querySelectorAll(focusableSelectors.join(","))); } // Focus trap implementation trapFocus(event, modal) { const container = modal.closest('pageblock-modal, [data-pb-modal="container"]') || this.findModalContainer(modal.getAttribute("data-modal-id")); if (!container) return; const state = this.modalState.get(container); if (!state) return; const focusableElements = state.focusableElements; if (focusableElements.length === 0) return; const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; if (event.shiftKey) { if (document.activeElement === firstElement) { lastElement.focus(); event.preventDefault(); } } else { if (document.activeElement === lastElement) { firstElement.focus(); event.preventDefault(); } } } // Public API methods open(modalId, options = {}) { this.openModal(modalId, options); } close(modalId, options = {}) { this.closeModal(modalId, options); } closeAll() { this.closeAllModals(); } destroy() { document.removeEventListener("click", this.handleDocumentClick); document.removeEventListener("keydown", this.handleKeyDown); document.removeEventListener("pb:modal:open", this.handleCustomOpenEvent); document.removeEventListener("pb:modal:close", this.handleCustomCloseEvent); this.closeAllModals(); this.modalState = /* @__PURE__ */ new WeakMap(); this.activeModals = []; this.log("Modal system destroyed"); } } export { Modal, Modal as default }; //# sourceMappingURL=modal.js.map