/** * Spectra Modal Script * * Wrapped in an IIFE with a load guard to prevent errors when the script * is inadvertently loaded multiple times (e.g., slider and modal blocks on same page). */ ( function() { 'use strict'; // Prevent double execution if script is loaded multiple times. if ( window.spectraModalScriptLoaded ) { return; } window.spectraModalScriptLoaded = true; // Constants const CLOSE_BUTTON_RENDER_DELAY = 50; // Delay for close button to render // Store modal-specific data const modalHandlers = new Map(); /** * Get the correct document element (handles iframe-based themes like Twenty Twenty-Five) * This is critical for theme compatibility */ function getDocumentElement() { // For frontend, always use main document (Twenty Twenty-Five doesn't use iframe on frontend) if ( !document.body.classList.contains( 'wp-admin' ) && !document.body.classList.contains( 'block-editor-page' ) ) { return document; } // For editor, check for iframe let document_element = document; const getEditorIframe = document.querySelectorAll( 'iframe[name="editor-canvas"]' ); if ( getEditorIframe?.length ) { const iframeDocument = getEditorIframe?.[ 0 ]?.contentWindow?.document || getEditorIframe?.[ 0 ]?.contentDocument; if ( iframeDocument ) { document_element = iframeDocument; } } return document_element; } /** * Attach global handlers for modal accessibility and interaction * Need to attach to BOTH main document AND iframe for full coverage */ function attachGlobalModalHandlers() { // Always attach to main document (for trigger elements) attachKeyboardHandlersToDocument( document ); // Also attach to iframe document if it exists (for Twenty Twenty-Five theme) const iframe = document.querySelector( 'iframe[name="editor-canvas"]' ); if ( iframe ) { const iframeDoc = iframe.contentWindow?.document || iframe.contentDocument; if ( iframeDoc ) { attachKeyboardHandlersToDocument( iframeDoc ); } else { // Iframe not loaded yet, wait for it iframe.addEventListener( 'load', () => { const iframeDocLoaded = iframe.contentWindow?.document || iframe.contentDocument; if ( iframeDocLoaded ) { attachKeyboardHandlersToDocument( iframeDocLoaded ); } } ); } } else { // Try with delay to catch dynamically created iframes setTimeout( () => { const delayedIframe = document.querySelector( 'iframe[name="editor-canvas"]' ); if ( delayedIframe ) { const iframeDoc = delayedIframe.contentWindow?.document || delayedIframe.contentDocument; if ( iframeDoc && !iframeDoc._spectraHandlersAttached ) { attachKeyboardHandlersToDocument( iframeDoc ); } } }, 1000 ); } } /** * Attach keyboard handlers to a specific document * * @param {Document} doc - The document to attach handlers to */ function attachKeyboardHandlersToDocument( doc ) { // Prevent duplicate attachment if ( doc._spectraHandlersAttached ) { return; } doc._spectraHandlersAttached = true; // Global keyboard handler for modal triggers and close buttons doc.addEventListener( 'keydown', ( e ) => { // Only element targets carry classList/getAttribute. A keydown dispatched // on the document itself (or a text node) has neither and used to throw here. const target = e.target; if ( ! target || typeof target.getAttribute !== 'function' || ! target.classList ) { return; } // 1. Handle modal trigger elements (Enter/Space to open) if ( ( target.classList.contains( 'modal-trigger-element' ) || target.getAttribute( 'data-wp-on--click' ) === 'spectra/modal::actions.toggle' || target.getAttribute( 'data-wp-on--click' ) === 'spectra/modal::actions.open' ) ) { if ( e.key === 'Enter' || e.key === ' ' ) { e.preventDefault(); // Just trigger a click event - let the existing click handler do the work target.click(); } } // 2. Handle close button elements (Enter/Space to close) else if ( target.classList.contains( 'spectra-modal-popup-close' ) || target.getAttribute( 'data-wp-on--click' ) === 'spectra/modal::actions.close' ) { if ( e.key === 'Enter' || e.key === ' ' ) { e.preventDefault(); // Just trigger a click event target.click(); } } } ); // ESC key handler - attach to both documents to catch ESC from anywhere doc.addEventListener( 'keydown', ( e ) => { if ( e.key === 'Escape' ) { // Look for active modal - the active class is on the popup element, not wrapper let activeModal = document.querySelector( '.spectra-modal-popup.active' ); // If not found in main document, check iframe if ( !activeModal ) { const iframe = document.querySelector( 'iframe[name="editor-canvas"]' ); if ( iframe ) { const iframeDoc = iframe.contentWindow?.document || iframe.contentDocument; if ( iframeDoc ) { activeModal = iframeDoc.querySelector( '.spectra-modal-popup.active' ); } } } if ( activeModal && activeModal.id ) { // Check if ESC key is enabled for this modal const escPress = activeModal.getAttribute( 'data-esc-press' ); // Only close if ESC is explicitly enabled. if ( escPress !== 'true' ) { return; // Don't close if ESC is not explicitly enabled } e.preventDefault(); e.stopPropagation(); // Use closeModal function if available, otherwise manual close if ( typeof closeModal === 'function' ) { closeModal( activeModal.id ); } else { activeModal.classList.remove( 'active' ); } } } } ); // Note: Overlay click is handled by attachOverlayClickClose function // This global handler is kept minimal to avoid conflicts } // Attach global modal handlers immediately when script loads // This ensures all modal interactions work even if modal initialization events don't fire properly if ( document.readyState === 'loading' ) { document.addEventListener( 'DOMContentLoaded', attachGlobalModalHandlers ); } else { attachGlobalModalHandlers(); } // Listen for modal initialization events and attach behaviours. // Listen on both main document and iframe document for full compatibility function setupModalInitializationListener() { const handleInitialization = ( event ) => { const { blockId, overlayClick, escPress, } = event.detail; // Normalize values into args with default fallbacks. const args = { blockId, overlayClick: overlayClick || false, escPress: escPress || false, }; // Clean up any existing handlers for this modal cleanupModalHandlers( blockId ); // Attach the necessary modal behavior handlers. // Close modal on overlay click. attachOverlayClickClose( args ); // Close modal on Escape key. attachEscKeyClose( args ); // Trap focus inside modal and manage keyboard accessibility attachKeyboardHandlers( args ); // Add keyboard support for close button attachCloseButtonKeyboard( args ); }; // Listen on main document document.addEventListener( 'spectra:modal:initialized', handleInitialization ); // Also listen on iframe document if exists const iframe = document.querySelector( 'iframe[name="editor-canvas"]' ); if ( iframe ) { const iframeDoc = iframe.contentWindow?.document || iframe.contentDocument; if ( iframeDoc ) { iframeDoc.addEventListener( 'spectra:modal:initialized', handleInitialization ); } } } // Setup listeners when DOM is ready if ( document.readyState === 'loading' ) { document.addEventListener( 'DOMContentLoaded', setupModalInitializationListener ); } else { setupModalInitializationListener(); } /** * Clean up any existing event handlers for a modal * * @param {string} blockId - The modal block ID */ function cleanupModalHandlers( blockId ) { const handlers = modalHandlers.get( blockId ); if ( handlers ) { // Remove all stored event listeners handlers.forEach( ( { element, event, handler } ) => { element.removeEventListener( event, handler ); } ); modalHandlers.delete( blockId ); } } /** * Store an event handler for later cleanup * * @param {string} blockId - The modal block ID * @param {Element} element - The DOM element * @param {string} event - The event name * @param {Function} handler - The event handler function */ function storeHandler( blockId, element, event, handler ) { if ( !modalHandlers.has( blockId ) ) { modalHandlers.set( blockId, [] ); } modalHandlers.get( blockId ).push( { element, event, handler } ); element.addEventListener( event, handler ); } /** * Attach handler to close modal when clicking outside modal content (on the overlay). * * @param {Object} args - Contains modal settings like blockId and overlayClick. */ function attachOverlayClickClose( args ) { const modal = getDocumentElement().getElementById( args.blockId ); if ( !modal ) { return; } if ( args.overlayClick ) { const handler = ( e ) => { // The modal structure is: //