PageSourceSearch

https://www.ziptrak.com.au/wp-content/themes/wp-theme-ziptrak/incl…/components/modal.service.js?ver=1791177423

js ziptrak.com.au collected 2026-10-08 01:07:21 UTC 11,419 bytes, 417 lines download raw bytes

1/**
2 * TODO: This file was compiled manually from TypeScript.
3 * In the future, set up Webpack to handle TS compilation automatically.
4 */
5
6/**
7 * Global Modal Service for Ziptrak Project
8 */
9
10// Configuration for selectors and CSS classes
11const MODAL_CONFIG = {
12	selectors: {
13		root: '#ziptrak-modal-root',
14		templateBase: '#ziptrak-tmpl-modal-',
15		trigger: '[data-ziptrak-modal-trigger]',
16		closeButton: '.ziptrak-modal-close, .ziptrak-modal-back',
17		container: '.ziptrak-modal-container',
18		focusable: [
19			'a[href]',
20			'button:not([disabled])',
21			'input:not([disabled])',
22			'select:not([disabled])',
23			'textarea:not([disabled])',
24			'[tabindex]:not([tabindex="-1"])',
25		].join(','),
26		slider: {
27			sliderSection: '.ziptrak-modal-slider-section',
28			swiperContainer: '.swiper',
29			swiperWrapper: '.swiper-wrapper',
30			btnPrev: '.swiper-button-prev',
31			btnNext: '.swiper-button-next',
32			pagination: '.swiper-pagination',
33		},
34		templates: {
35			testimonial: {
36				text: '.testimonial-text',
37				author: '.testimonial-author',
38				location: '.testimonial-location',
39				stars: '.testimonial-stars',
40				sliderSection: '.ziptrak-modal-slider-section',
41				sliderImage: '.slider-current-image',
42				sliderDots: '.slider-dots',
43				sliderDot: '.slider-dot',
44				sliderControls: '.slider-controls',
45				btnPrev: '.slider-prev',
46				btnNext: '.slider-next',
47			},
48			blindGallery: {
49				sliderSection: '.ziptrak-modal-slider-section',
50				sliderImage: '.slider-current-image',
51				sliderDots: '.slider-dots',
52				sliderDot: '.slider-dot',
53				sliderControls: '.slider-controls',
54				btnPrev: '.slider-prev',
55				btnNext: '.slider-next',
56			},
57			overview: {
58				label: '.overview__label',
59				title: '.overview__title',
60				description: '.overview__description',
61				list: '.overview__list',
62			}
63		}
64	},
65	classes: {
66		active: 'is-active',
67		hasImages: 'has-images',
68		noScroll: 'no-scroll'
69	}
70};
71
72class ZiptrakModalService {
73	root;
74	swiperInstance = null;
75	activeTrigger = null;
76	returnFocusElement = null;
77	inlineSource = null;
78	inlineContent = null;
79	setupTemplate = {
80		testimonial: (data) => this.setupTestimonial(data),
81		gallery: (data) => this.setupGallery(data),
82		overview: (data) => this.setupOverview(data),
83	};
84
85	constructor() {
86		this.root = document.querySelector(MODAL_CONFIG.selectors.root);
87		this.initGlobalEvents();
88	}
89
90	/**
91	 * Initialize static event listeners
92	 */
93	initGlobalEvents() {
94		if (!this.root) {
95			return;
96		}
97		this.root.addEventListener('click', this.handleRootClick);
98		document.addEventListener('click', this.handleDocumentClick);
99		document.addEventListener('keydown', this.handleKeyDown);
100	}
101
102	/**
103	 * Open editor-authored content in the shared modal window.
104	 */
105	handleDocumentClick = (event) => {
106		const target = event.target;
107		if (!(target instanceof Element)) {
108			return;
109		}
110		const trigger = target.closest(MODAL_CONFIG.selectors.trigger);
111		if (!trigger) {
112			return;
113		}
114		const sourceId = trigger.dataset.ziptrakModalTarget || trigger.getAttribute('aria-controls');
115		const source = sourceId ? document.getElementById(sourceId) : null;
116		if (!source) {
117			return;
118		}
119		event.preventDefault();
120		this.openInlineContent(source, trigger);
121	};
122
123	/**
124	 * Handle clicks outside the content area (on backdrop/root)
125	 */
126	handleRootClick = (e) => {
127		const target = e.target;
128		if (target === this.root) {
129			this.close();
130			return;
131		}
132		if (target instanceof Element && target.closest(MODAL_CONFIG.selectors.closeButton)) {
133			this.close();
134		}
135	};
136	/**
137	 * Handle global keyboard navigation (ESC key)
138	 */
139	handleKeyDown = (e) => {
140		if (!this.root?.classList.contains(MODAL_CONFIG.classes.active)) {
141			return;
142		}
143		if (e.key === 'Escape') {
144			e.preventDefault();
145			this.close();
146		} else if (e.key === 'Tab') {
147			this.trapFocus(e);
148		}
149	};
150
151	/**
152	 * Open modal by template ID and inject data
153	 */
154	open(templateId, data) {
155		if (!this.root) {
156			return;
157		}
158		const template = document.querySelector(`${MODAL_CONFIG.selectors.templateBase}${templateId}`);
159		if (!template) {
160			console.warn(`Template with id ${templateId} not found`);
161			return;
162		}
163		// 1. Clear and render new content
164		this.prepareRoot();
165		const clone = template.content.cloneNode(true);
166		this.root.appendChild(clone);
167		// 2. Initialize specific logic based on template type
168		this.setupTemplate[templateId](data);
169		// 3. Show modal and lock scroll
170		this.finishOpen(null, templateId);
171	}
172
173	/**
174	 * Move the live inner-block content into the shared modal root. Moving the
175	 * element preserves any state and listeners initialized by nested blocks.
176	 */
177	openInlineContent(source, trigger) {
178		if (!this.root) {
179			return;
180		}
181		const content = source.querySelector(MODAL_CONFIG.selectors.container);
182		if (!content) {
183			return;
184		}
185		this.prepareRoot();
186		this.inlineSource = source;
187		this.inlineContent = content;
188		this.root.appendChild(content);
189		this.finishOpen(trigger, 'content');
190	}
191
192	/**
193	 * Close modal and clear content after animation
194	 */
195	close() {
196		if (!this.root || !this.root.classList.contains(MODAL_CONFIG.classes.active)) {
197			return;
198		}
199		this.destroySwiper();
200		this.toggleModalState(false);
201		const closedDialog = this.root.querySelector(MODAL_CONFIG.selectors.container);
202		this.restoreInlineContent();
203		this.restoreTrigger();
204		this.dispatchModalEvent('closed', {dialog: closedDialog});
205	}
206
207	prepareRoot() {
208		this.destroySwiper();
209		this.clearTriggerState();
210		this.restoreInlineContent();
211		if (this.root) {
212			this.root.innerHTML = '';
213		}
214	}
215
216	finishOpen(trigger, templateId) {
217		if (!this.root) {
218			return;
219		}
220		this.setupTriggerState(trigger);
221		const dialog = this.root.querySelector(MODAL_CONFIG.selectors.container);
222		this.setupDialogAccessibility(dialog);
223		this.toggleModalState(true);
224		this.setInitialFocus(dialog);
225		this.dispatchModalOpenedEvents(templateId, dialog);
226	}
227
228	setupTriggerState(trigger) {
229		this.activeTrigger = trigger;
230		this.returnFocusElement = trigger || (document.activeElement instanceof HTMLElement ? document.activeElement : null);
231		this.activeTrigger?.setAttribute('aria-expanded', 'true');
232	}
233
234	setupDialogAccessibility(dialog) {
235		if (dialog) {
236			dialog.setAttribute('role', 'dialog');
237			dialog.setAttribute('aria-modal', 'true');
238			if (!dialog.hasAttribute('tabindex')) {
239				dialog.tabIndex = -1;
240			}
241		}
242	}
243
244	setInitialFocus(dialog) {
245		if (!this.root) {
246			return;
247		}
248		const initialFocus = this.root.querySelector(MODAL_CONFIG.selectors.closeButton) || dialog;
249		initialFocus?.focus({preventScroll: true});
250	}
251
252	dispatchModalEvent(eventName, detail) {
253		window.dispatchEvent(new CustomEvent(`ziptrak:modal-${eventName}`, {detail}));
254	}
255
256	dispatchModalOpenedEvents(templateId, dialog) {
257		this.dispatchModalEvent('opened', {templateId, dialog});
258		window.dispatchEvent(new Event('resize'));
259	}
260
261	clearTriggerState() {
262		this.activeTrigger?.setAttribute('aria-expanded', 'false');
263		this.activeTrigger = null;
264		this.returnFocusElement = null;
265	}
266
267	restoreTrigger() {
268		const triggerToRestore = this.returnFocusElement;
269		this.clearTriggerState();
270		triggerToRestore?.focus({preventScroll: true});
271	}
272
273	restoreInlineContent() {
274		if (this.inlineSource && this.inlineContent) {
275			this.inlineSource.appendChild(this.inlineContent);
276		}
277		this.inlineSource = null;
278		this.inlineContent = null;
279	}
280
281	trapFocus(event) {
282		if (!this.root) {
283			return;
284		}
285		const focusable = Array.from(this.root.querySelectorAll(MODAL_CONFIG.selectors.focusable))
286			.filter((element) => !element.hasAttribute('hidden') && element.offsetParent !== null);
287		if (!focusable.length) {
288			event.preventDefault();
289			this.root.querySelector(MODAL_CONFIG.selectors.container)?.focus({preventScroll: true});
290			return;
291		}
292		const first = focusable[0];
293		const last = focusable[focusable.length - 1];
294		if (event.shiftKey && document.activeElement === first) {
295			event.preventDefault();
296			last.focus({preventScroll: true});
297		} else if (!event.shiftKey && document.activeElement === last) {
298			event.preventDefault();
299			first.focus({preventScroll: true});
300		}
301	}
302
303	setText(selector, val) {
304		const element = this.root.querySelector(selector);
305		if (element) {
306			element.textContent = val || '';
307		}
308	}
309	;
310
311	/**
312	 * Toggles modal active state and body scroll lock
313	 * @param isOpen - Pass true to open, false to close
314	 */
315	toggleModalState(isOpen) {
316		if (!this.root) {
317			return;
318		}
319		this.root.classList.toggle(MODAL_CONFIG.classes.active, isOpen);
320		this.root.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
321		document.body.classList.toggle(MODAL_CONFIG.classes.noScroll, isOpen);
322	}
323
324	/**
325	 * Specifically handles testimonial content
326	 */
327	setupTestimonial(data) {
328		const selectors = MODAL_CONFIG.selectors.templates.testimonial;
329		this.setText(selectors.text, data.fullText);
330		this.setText(selectors.author, data.author);
331		this.setText(selectors.location, data.location);
332		const starsEl = this.root?.querySelector(selectors.stars);
333		if (starsEl) {
334			starsEl.innerHTML = Array.from({length: data.rating}, () => '<div class="star-icon"></div>').join('');
335		}
336		this.setupSlider(JSON.parse(data.images || '[]'));
337	}
338
339	/**
340	 * Handles Gallery conent
341	 */
342	setupGallery(data) {
343		this.setupSlider(data.images || [], data.index);
344	}
345
346	/**
347	 * Handles overview content
348	 */
349	setupOverview(data) {
350		const selectors = MODAL_CONFIG.selectors.templates.overview;
351		this.setText(selectors.label, data.modalLabel);
352		this.setText(selectors.title, data.modalTitle);
353		this.setText(selectors.description, data.modalDescription);
354		// Render list items
355		const listContainer = this.root?.querySelector(selectors.list);
356		if (listContainer) {
357			listContainer.innerHTML = data.modalList.map((text) => `<li class="list-item-warning">${text}</li>`).join('');
358		}
359		this.setupSlider(data.modalImages || []);
360	}
361
362	/**
363	 * Setup Slider
364	 */
365	setupSlider(images, initialIndex = 0) {
366		if (!this.root) {
367			return;
368		}
369		const selectors = MODAL_CONFIG.selectors.slider;
370		const sliderSection = this.root.querySelector(selectors.sliderSection);
371		const swiperElement = this.root.querySelector(selectors.swiperContainer);
372		const wrapper = this.root.querySelector(selectors.swiperWrapper);
373		const container = this.root.querySelector(MODAL_CONFIG.selectors.container);
374		if (!images?.length || !sliderSection || !wrapper || !swiperElement) {
375			if (sliderSection) {
376				sliderSection.style.display = 'none';
377			}
378			return;
379		}
380		sliderSection.style.display = '';
381		container?.classList.add(MODAL_CONFIG.classes.hasImages);
382		wrapper.innerHTML = images
383				.map((src) => `<div class="swiper-slide"><img src="${src}" alt="Modal image" /></div>`)
384				.join('');
385		this.swiperInstance = new Swiper(swiperElement, {
386			initialSlide: initialIndex,
387			loop: false,
388			spaceBetween: 20,
389			slidesPerView: 1,
390			pagination: {
391				el: this.root.querySelector(selectors.pagination),
392				clickable: true,
393			},
394			navigation: {
395				nextEl: this.root.querySelector(selectors.btnNext),
396				prevEl: this.root.querySelector(selectors.btnPrev),
397			},
398		});
399	}
400
401	destroySwiper() {
402		if (this.swiperInstance) {
403			this.swiperInstance.destroy(true, true);
404			this.swiperInstance = null;
405		}
406	}
407}
408
409function initializeModalService() {
410	window.ModalService = new ZiptrakModalService();
411}
412
413if (document.readyState === 'loading') {
414	document.addEventListener('DOMContentLoaded', initializeModalService, {once: true});
415} else {
416	initializeModalService();
417}

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.