PageSourceSearch

https://www.ultimatebeaver.com/wp-content/plugins/ultimate-addons-…/assets/js/modal-script.js?ver=3.0.0-beta.2

js ultimatebeaver.com collected 2026-10-03 21:37:47 UTC 17,765 bytes, 550 lines download raw bytes

1/**
2 * Spectra Modal Script
3 *
4 * Wrapped in an IIFE with a load guard to prevent errors when the script
5 * is inadvertently loaded multiple times (e.g., slider and modal blocks on same page).
6 */
7( function() {
8	'use strict';
9
10	// Prevent double execution if script is loaded multiple times.
11	if ( window.spectraModalScriptLoaded ) {
12		return;
13	}
14	window.spectraModalScriptLoaded = true;
15
16	// Constants
17	const CLOSE_BUTTON_RENDER_DELAY = 50; // Delay for close button to render
18
19	// Store modal-specific data
20	const modalHandlers = new Map();
21
22/**
23 * Get the correct document element (handles iframe-based themes like Twenty Twenty-Five)
24 * This is critical for theme compatibility
25 */
26function getDocumentElement() {
27	// For frontend, always use main document (Twenty Twenty-Five doesn't use iframe on frontend)
28	if ( !document.body.classList.contains( 'wp-admin' ) && !document.body.classList.contains( 'block-editor-page' ) ) {
29		return document;
30	}
31	
32	// For editor, check for iframe
33	let document_element = document;
34	const getEditorIframe = document.querySelectorAll( 'iframe[name="editor-canvas"]' );
35	
36	if ( getEditorIframe?.length ) {
37		const iframeDocument =
38			getEditorIframe?.[ 0 ]?.contentWindow?.document || getEditorIframe?.[ 0 ]?.contentDocument;
39		if ( iframeDocument ) {
40			document_element = iframeDocument;
41		}
42	}
43	return document_element;
44}
45
46/**
47 * Attach global handlers for modal accessibility and interaction
48 * Need to attach to BOTH main document AND iframe for full coverage
49 */
50function attachGlobalModalHandlers() {
51	// Always attach to main document (for trigger elements)
52	attachKeyboardHandlersToDocument( document );
53	
54	// Also attach to iframe document if it exists (for Twenty Twenty-Five theme)
55	const iframe = document.querySelector( 'iframe[name="editor-canvas"]' );
56	if ( iframe ) {
57		const iframeDoc = iframe.contentWindow?.document || iframe.contentDocument;
58		if ( iframeDoc ) {
59			attachKeyboardHandlersToDocument( iframeDoc );
60		} else {
61			// Iframe not loaded yet, wait for it
62			iframe.addEventListener( 'load', () => {
63				const iframeDocLoaded = iframe.contentWindow?.document || iframe.contentDocument;
64				if ( iframeDocLoaded ) {
65					attachKeyboardHandlersToDocument( iframeDocLoaded );
66				}
67			} );
68		}
69	} else {
70		// Try with delay to catch dynamically created iframes
71		setTimeout( () => {
72			const delayedIframe = document.querySelector( 'iframe[name="editor-canvas"]' );
73			if ( delayedIframe ) {
74				const iframeDoc = delayedIframe.contentWindow?.document || delayedIframe.contentDocument;
75				if ( iframeDoc && !iframeDoc._spectraHandlersAttached ) {
76					attachKeyboardHandlersToDocument( iframeDoc );
77				}
78			}
79		}, 1000 );
80	}
81}
82
83/**
84 * Attach keyboard handlers to a specific document
85 *
86 * @param {Document} doc - The document to attach handlers to
87 */
88function attachKeyboardHandlersToDocument( doc ) {
89	// Prevent duplicate attachment
90	if ( doc._spectraHandlersAttached ) {
91		return;
92	}
93	doc._spectraHandlersAttached = true;
94	
95	
96	// Global keyboard handler for modal triggers and close buttons
97	doc.addEventListener( 'keydown', ( e ) => {
98		// 1. Handle modal trigger elements (Enter/Space to open)
99		if ( ( e.target.classList.contains( 'modal-trigger-element' ) || 
100			   e.target.getAttribute( 'data-wp-on--click' ) === 'spectra/modal::actions.toggle' ||
101			   e.target.getAttribute( 'data-wp-on--click' ) === 'spectra/modal::actions.open' ) ) {
102			
103			if ( e.key === 'Enter' || e.key === ' ' ) {
104				e.preventDefault();
105				
106				// Just trigger a click event - let the existing click handler do the work
107				e.target.click();
108			}
109		}
110		
111		// 2. Handle close button elements (Enter/Space to close)
112		else if ( e.target.classList.contains( 'spectra-modal-popup-close' ) || 
113				  e.target.getAttribute( 'data-wp-on--click' ) === 'spectra/modal::actions.close' ) {
114			
115			if ( e.key === 'Enter' || e.key === ' ' ) {
116				e.preventDefault();
117				
118				// Just trigger a click event
119				e.target.click();
120			}
121		}
122	} );
123	
124	// ESC key handler - attach to both documents to catch ESC from anywhere
125	doc.addEventListener( 'keydown', ( e ) => {
126		if ( e.key === 'Escape' ) {
127			// Look for active modal - the active class is on the popup element, not wrapper
128			let activeModal = document.querySelector( '.spectra-modal-popup.active' );
129			
130			// If not found in main document, check iframe
131			if ( !activeModal ) {
132				const iframe = document.querySelector( 'iframe[name="editor-canvas"]' );
133				if ( iframe ) {
134					const iframeDoc = iframe.contentWindow?.document || iframe.contentDocument;
135					if ( iframeDoc ) {
136						activeModal = iframeDoc.querySelector( '.spectra-modal-popup.active' );
137					}
138				}
139			}
140			
141			if ( activeModal && activeModal.id ) {
142				// Check if ESC key is enabled for this modal
143				const escPress = activeModal.getAttribute( 'data-esc-press' );
144				
145				// Only close if ESC is explicitly enabled.
146				if ( escPress !== 'true' ) {
147					return; // Don't close if ESC is not explicitly enabled
148				}
149				
150				e.preventDefault();
151				e.stopPropagation();
152				
153				// Use closeModal function if available, otherwise manual close
154				if ( typeof closeModal === 'function' ) {
155					closeModal( activeModal.id );
156				} else {
157					activeModal.classList.remove( 'active' );
158				}
159			}
160		}
161	} );
162	
163	// Note: Overlay click is handled by attachOverlayClickClose function
164	// This global handler is kept minimal to avoid conflicts
165}
166
167// Attach global modal handlers immediately when script loads
168// This ensures all modal interactions work even if modal initialization events don't fire properly
169if ( document.readyState === 'loading' ) {
170	document.addEventListener( 'DOMContentLoaded', attachGlobalModalHandlers );
171} else {
172	attachGlobalModalHandlers();
173}
174
175// Listen for modal initialization events and attach behaviours.
176// Listen on both main document and iframe document for full compatibility
177function setupModalInitializationListener() {
178	const handleInitialization = ( event ) => {
179		const {
180			blockId,
181			overlayClick,
182			escPress,
183		} = event.detail;
184
185		// Normalize values into args with default fallbacks.
186		const args = {
187			blockId,
188			overlayClick: overlayClick || false,
189			escPress: escPress || false,
190		};
191
192		// Clean up any existing handlers for this modal
193		cleanupModalHandlers( blockId );
194
195		// Attach the necessary modal behavior handlers.
196		// Close modal on overlay click.
197		attachOverlayClickClose( args );
198		// Close modal on Escape key.
199		attachEscKeyClose( args );
200		// Trap focus inside modal and manage keyboard accessibility
201		attachKeyboardHandlers( args );
202		// Add keyboard support for close button
203		attachCloseButtonKeyboard( args );
204	};
205
206	// Listen on main document
207	document.addEventListener( 'spectra:modal:initialized', handleInitialization );
208	
209	// Also listen on iframe document if exists
210	const iframe = document.querySelector( 'iframe[name="editor-canvas"]' );
211	if ( iframe ) {
212		const iframeDoc = iframe.contentWindow?.document || iframe.contentDocument;
213		if ( iframeDoc ) {
214			iframeDoc.addEventListener( 'spectra:modal:initialized', handleInitialization );
215		}
216	}
217}
218
219// Setup listeners when DOM is ready
220if ( document.readyState === 'loading' ) {
221	document.addEventListener( 'DOMContentLoaded', setupModalInitializationListener );
222} else {
223	setupModalInitializationListener();
224}
225
226
227/**
228 * Clean up any existing event handlers for a modal
229 *
230 * @param {string} blockId - The modal block ID
231 */
232function cleanupModalHandlers( blockId ) {
233	const handlers = modalHandlers.get( blockId );
234	if ( handlers ) {
235		// Remove all stored event listeners
236		handlers.forEach( ( { element, event, handler } ) => {
237			element.removeEventListener( event, handler );
238		} );
239		modalHandlers.delete( blockId );
240	}
241}
242
243/**
244 * Store an event handler for later cleanup
245 *
246 * @param {string} blockId - The modal block ID
247 * @param {Element} element - The DOM element
248 * @param {string} event - The event name
249 * @param {Function} handler - The event handler function
250 */
251function storeHandler( blockId, element, event, handler ) {
252	if ( !modalHandlers.has( blockId ) ) {
253		modalHandlers.set( blockId, [] );
254	}
255	modalHandlers.get( blockId ).push( { element, event, handler } );
256	element.addEventListener( event, handler );
257}
258
259/**
260 * Attach handler to close modal when clicking outside modal content (on the overlay).
261 *
262 * @param {Object} args - Contains modal settings like blockId and overlayClick.
263 */
264function attachOverlayClickClose( args ) {
265	const modal = getDocumentElement().getElementById( args.blockId );
266	if ( !modal ) {
267		return;
268	}
269	if ( args.overlayClick ) {
270		const handler = ( e ) => {
271			// The modal structure is: 
272			// <div id="blockId" class="active"> <!-- modal wrapper -->
273			//   <div class="spectra-modal-popup active"> <!-- popup/overlay -->
274			//     <div class="spectra-modal-popup-wrap"> <!-- content wrapper -->
275			
276			// If click target is the wrapper (modal element itself), close it
277			if ( e.currentTarget === modal && e.target === modal ) {
278				closeModal( args.blockId );
279				return;
280			}
281			
282			// Find the popup element
283			const popup = modal.querySelector( '.spectra-modal-popup' );
284			if ( !popup || !popup.classList.contains( 'active' ) ) return;
285			
286			const popupWrap = popup.querySelector( '.spectra-modal-popup-wrap' );
287			if ( !popupWrap ) return;
288			
289			// Click is on overlay if click is on popup but not inside popup-wrap
290			if ( e.target === popup ) {
291				closeModal( args.blockId );
292			}
293		};
294		
295		// Attach to both modal wrapper and popup for complete coverage
296		storeHandler( args.blockId, modal, 'click', handler );
297		
298		const popup = modal.querySelector( '.spectra-modal-popup' );
299		if ( popup ) {
300			const popupHandler = ( e ) => {
301				const popupWrap = popup.querySelector( '.spectra-modal-popup-wrap' );
302				if ( popupWrap && e.target === popup ) {
303					e.stopPropagation();
304					closeModal( args.blockId );
305				}
306			};
307			storeHandler( args.blockId, popup, 'click', popupHandler );
308		}
309	}
310}
311
312/**
313 * Attach handler to close modal when Escape key is pressed.
314 *
315 * @param {Object} args - Contains modal settings including escPress flag.
316 */
317function attachEscKeyClose( args ) {
318	if ( args.escPress ) {
319		const handler = ( e ) => {
320			if ( e.key === 'Escape' ) {
321				const modal = getDocumentElement().getElementById( args.blockId );
322				if ( modal && modal.classList.contains( 'active' ) ) {
323					closeModal( args.blockId );
324				}
325			}
326		};
327		storeHandler( args.blockId, getDocumentElement(), 'keydown', handler );
328	}
329}
330
331/**
332 * Close the modal by hiding it and removing active state.
333 *
334 * @param {string} blockId - The modal block ID to close.
335 */
336function closeModal( blockId ) {
337	const modal = getDocumentElement().getElementById( blockId );
338	if ( ! modal ) return;
339
340	modal.classList.remove( 'active' );
341	
342	// Also remove active from popup element if it exists
343	const popup = modal.querySelector( '.spectra-modal-popup' );
344	if ( popup ) {
345		popup.classList.remove( 'active' );
346		popup.setAttribute( 'aria-hidden', 'true' );
347		popup.removeAttribute( 'aria-modal' );
348	}
349	
350	// Restore focus to previously focused element
351	const state = modalHandlers.get( blockId )?.state;
352	if ( state?.previouslyFocusedElement ) {
353		// Use setTimeout to ensure focus is restored after modal is fully closed
354		setTimeout( () => {
355			if ( state.previouslyFocusedElement && typeof state.previouslyFocusedElement.focus === 'function' ) {
356				state.previouslyFocusedElement.focus();
357			}
358		}, 0 );
359	}
360	
361	// Dispatch close event for any cleanup
362	const event = new CustomEvent( 'spectra:modal:closed', {
363		detail: { blockId },
364		bubbles: true
365	} );
366	document.dispatchEvent( event );
367}
368
369/**
370 * Enable keyboard accessibility for modal including focus trap and restoring focus.
371 *
372 * @param {Object} args - Modal configuration.
373 */
374function attachKeyboardHandlers( args ) {
375	// Store modal-specific state in the handlers map
376	if ( !modalHandlers.has( args.blockId ) ) {
377		modalHandlers.set( args.blockId, [] );
378	}
379	
380	// Add state properties
381	modalHandlers.get( args.blockId ).state = {
382		previouslyFocusedElement: null,
383		isFirstTab: true
384	};
385
386	// Trap tab focus inside the modal
387	const keydownHandler = ( e ) => {
388		const modal = getDocumentElement().getElementById( args.blockId );
389		if ( ! modal || ! modal.classList.contains( 'active' ) ) return;
390		
391		// Check if the popup element is hidden (not the wrapper)
392		const popup = modal.querySelector( '.spectra-modal-popup' );
393		if ( popup && popup.getAttribute( 'aria-hidden' ) === 'true' ) return;
394		
395		const state = modalHandlers.get( args.blockId )?.state;
396		if ( ! state ) return;
397
398		const focusable = modal.querySelectorAll(
399			'a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])'
400		);
401		if ( !focusable.length ) return;
402
403		const first = focusable[0];
404		const last = focusable[focusable.length - 1];
405		const activeElement = modal.ownerDocument.activeElement;
406
407		if ( e.key === 'Tab' ) {
408			// Check if modal was triggered automatically and this is first tab
409			const modalTrigger = modal.getAttribute( 'data-modal-trigger' ) || 'manual';
410			
411			if ( state.isFirstTab && modalTrigger === 'automatic' && !modal.contains( activeElement ) ) {
412				// First tab on automatic modal - focus close button
413				e.preventDefault();
414				first.focus();
415				state.isFirstTab = false;
416				return;
417			}
418			
419			// Trap focus inside modal.
420			if ( e.shiftKey && activeElement === first ) {
421				e.preventDefault();
422				last.focus();
423			} else if ( !e.shiftKey && activeElement === last ) {
424				e.preventDefault();
425				first.focus();
426			} else if ( !modal.contains( activeElement ) ) {
427				// If focus is outside modal, bring it back inside
428				e.preventDefault();
429				if ( e.shiftKey ) {
430					last.focus();
431				} else {
432					first.focus();
433				}
434			}
435		} else if ( e.key === 'Escape' && args.escPress ) {
436			// Close modal and restore previous focus only if escPress is enabled.
437			closeModal( args.blockId );
438			if ( state.previouslyFocusedElement ) {
439				state.previouslyFocusedElement.focus();
440			}
441		}
442	};
443	storeHandler( args.blockId, getDocumentElement(), 'keydown', keydownHandler );
444	
445	// Prevent focus on elements outside modal when clicking
446	const focusinHandler = ( e ) => {
447		const modal = getDocumentElement().getElementById( args.blockId );
448		if ( ! modal || ! modal.classList.contains( 'active' ) ) return;
449		
450		// If focus moves outside the modal, bring it back
451		if ( !modal.contains( e.target ) ) {
452			e.preventDefault();
453			e.stopPropagation();
454			
455			const focusable = modal.querySelectorAll(
456				'a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])'
457			);
458			if ( focusable.length > 0 ) {
459				focusable[0].focus();
460			}
461		}
462	};
463	storeHandler( args.blockId, getDocumentElement(), 'focusin', focusinHandler );
464
465	// Set initial focus to first focusable element on modal open.
466	const openHandler = ( event ) => {
467		// Check if this event is for our modal
468		if ( event.detail?.blockId && event.detail.blockId !== args.blockId ) return;
469		
470		const modal = getDocumentElement().getElementById( args.blockId );
471		if ( !modal || !modal.classList.contains( 'active' ) ) return;
472		
473		const state = modalHandlers.get( args.blockId )?.state;
474		if ( ! state ) return;
475
476		const focusable = modal.querySelectorAll(
477			'a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])'
478		);
479		if ( !focusable.length ) return;
480		const activeElement = modal.ownerDocument.activeElement;
481
482		// Save currently focused element to return focus when modal closes.
483		// Prefer trigger element from event detail, otherwise use active element
484		if ( event.detail?.triggerElement ) {
485			state.previouslyFocusedElement = event.detail.triggerElement;
486		} else {
487			state.previouslyFocusedElement = activeElement;
488		}
489		
490		// Reset first tab flag
491		state.isFirstTab = true;
492		
493		// Get modal trigger type from event detail if available
494		const modalTrigger = event.detail?.modalTrigger || modal.getAttribute( 'data-modal-trigger' ) || 'manual';
495		
496		// Don't auto-focus any element when modal opens
497		// The user should control focus with Tab key
498		// Only exception: automatic modals might need different handling
499		if ( modalTrigger === 'automatic' ) {
500			// For automatic modals, we might want different behavior
501			// Currently, we don't focus anything
502		}
503	};
504	storeHandler( args.blockId, getDocumentElement(), 'spectra:modal:opened', openHandler );
505}
506
507/**
508 * Add keyboard support for close button (Enter/Space to close)
509 *
510 * @param {Object} args - Modal configuration.
511 */
512function attachCloseButtonKeyboard( args ) {
513	const modal = getDocumentElement().getElementById( args.blockId );
514	if ( !modal ) return;
515	
516	// Wait a bit for modal content to render
517	setTimeout( () => {
518		// Find close button with the specific class
519		const closeButtons = modal.querySelectorAll( '.spectra-modal-popup-close' );
520		
521		closeButtons.forEach( button => {
522			// Make sure the element is focusable if it's not a button/link
523			if ( button.tagName !== 'BUTTON' && button.tagName !== 'A' && !button.hasAttribute( 'tabindex' ) ) {
524				button.setAttribute( 'tabindex', '0' );
525			}
526			
527			// Add role button if not already a button
528			if ( button.tagName !== 'BUTTON' && !button.hasAttribute( 'role' ) ) {
529				button.setAttribute( 'role', 'button' );
530			}
531			
532			const handler = ( e ) => {
533				// Handle Enter key (Note: Tab should not trigger close, it's for navigation)
534				if ( e.key === 'Enter' || e.key === ' ' ) {
535					e.preventDefault();
536					e.stopPropagation();
537					
538					// Trigger click event on the close button
539					button.click();
540					
541					// Or directly close the modal (which will restore focus)
542					closeModal( args.blockId );
543				}
544			};
545			storeHandler( args.blockId, button, 'keydown', handler );
546		} );
547	}, CLOSE_BUTTON_RENDER_DELAY );
548}
549
550} )(); // End IIFE

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.