PageSourceSearch

https://www.paint.org/wp-content/themes/paint2025/assets/scripts/aca-modal.js?ver=1788963059

js paint.org collected 2026-09-24 09:30:46 UTC 7,634 bytes, 241 lines download raw bytes

1/*
2 * ============================================================================
3 * ACA MODAL SYSTEM v1.0
4 * ============================================================================
5 * 
6 * A modern, accessible modal system for the ACA website.
7 * 
8 * Features:
9 * - Modern ES6+ syntax with classes
10 * - Inline content support with data attribute linking
11 * - Content from other elements via selector
12 * - Close on overlay click and ESC key
13 * - Full accessibility with ARIA attributes and focus management
14 * - Fade animations
15 * - Multiple modal support
16 * 
17 * Usage:
18 * - Trigger: <button data-modal="modal-id">Open Modal</button>
19 * - Inline Content: <div id="modal-id" class="aca-modal-content">Content</div>
20 * - External Content: <button data-modal-content="#content-selector">Open</button>
21 * 
22 * Last updated: October 28, 2025, by GitHub Copilot
23 * ============================================================================
24 */
25
26class ACAModal {
27    constructor() {
28        this.activeModal = null;
29        this.previousFocus = null;
30        this.modalContainer = null;
31
32        this.init();
33    }
34
35    init() {
36        this.bindEvents();
37    }
38
39    /**
40     * Creates the modal container and overlay structure
41     */
42    createModalContainer() {
43        if (document.querySelector('.aca-modal-overlay')) return;
44
45        this.modalContainer = document.createElement('div');
46        this.modalContainer.className = 'aca-modal-overlay';
47        this.modalContainer.setAttribute('aria-hidden', 'true');
48        this.modalContainer.innerHTML = `
49            <div class="aca-modal-dialog" role="dialog" aria-modal="true">
50                <div class="aca-modal-content-wrapper">
51                    <button class="aca-modal-close" aria-label="Close modal" type="button">
52                        <span aria-hidden="true">&times;</span>
53                    </button>
54                    <div class="aca-modal-body"></div>
55                </div>
56            </div>
57        `;
58
59        document.body.appendChild(this.modalContainer);
60    }
61
62    /**
63     * Binds all event listeners
64     */
65    bindEvents() {
66        // Modal triggers
67        document.addEventListener('click', (e) => {
68            const trigger = e.target.closest('[data-modal], [data-modal-content]');
69            if (trigger) {
70                e.preventDefault();
71                this.openModal(trigger);
72            }
73
74            // Close button
75            if (e.target.closest('.aca-modal-close')) {
76                this.closeModal();
77            }
78
79            // Overlay click
80            if (e.target.classList.contains('aca-modal-overlay')) {
81                this.closeModal();
82            }
83        });
84
85        // ESC key close
86        document.addEventListener('keydown', (e) => {
87            if (e.key === 'Escape' && this.activeModal) {
88                this.closeModal();
89            }
90
91            // Trap focus within modal
92            if (e.key === 'Tab' && this.activeModal) {
93                this.trapFocus(e);
94            }
95        });
96    }
97
98    /**
99     * Opens a modal with the specified content
100     * @param {HTMLElement} trigger - The element that triggered the modal
101     */
102    openModal(trigger) {
103        // Lazy initialization - only create modal container when first needed
104        if (!this.modalContainer) {
105            this.createModalContainer();
106        }
107
108        const modalId = trigger.getAttribute('data-modal');
109        const contentSelector = trigger.getAttribute('data-modal-content');
110
111        let content = '';
112
113        if (modalId) {
114            // Inline content by ID
115            const contentElement = document.getElementById(modalId);
116            if (contentElement) {
117                content = contentElement.innerHTML;
118            } else {
119                console.warn(`Modal content with ID "${modalId}" not found`);
120                return;
121            }
122        } else if (contentSelector) {
123            // Content from selector
124            const contentElement = document.querySelector(contentSelector);
125            if (contentElement) {
126                content = contentElement.innerHTML;
127            } else {
128                console.warn(`Modal content with selector "${contentSelector}" not found`);
129                return;
130            }
131        } else {
132            console.warn('No modal content specified');
133            return;
134        }
135
136        // Store previous focus
137        this.previousFocus = document.activeElement;
138
139        // Set content
140        const modalBody = this.modalContainer.querySelector('.aca-modal-body');
141        modalBody.innerHTML = content;
142
143        // Show modal
144        this.modalContainer.classList.add('aca-modal-is-open');
145        this.modalContainer.setAttribute('aria-hidden', 'false');
146        document.body.classList.add('aca-modal-body-locked');
147
148        // Set focus to modal
149        const focusableElement = this.modalContainer.querySelector('.aca-modal-dialog');
150        focusableElement.focus();
151
152        this.activeModal = this.modalContainer;
153
154        // Trigger custom event
155        const event = new CustomEvent('aca:modal:opened', {
156            detail: { trigger, modalId, contentSelector }
157        });
158        document.dispatchEvent(event);
159    }
160
161    /**
162     * Closes the currently active modal
163     */
164    closeModal() {
165        if (!this.activeModal) return;
166
167        this.modalContainer.classList.remove('aca-modal-is-open');
168        this.modalContainer.setAttribute('aria-hidden', 'true');
169        document.body.classList.remove('aca-modal-body-locked');
170
171        // Restore focus
172        if (this.previousFocus) {
173            this.previousFocus.focus();
174            this.previousFocus = null;
175        }
176
177        // Clear content after animation
178        setTimeout(() => {
179            if (!this.modalContainer.classList.contains('aca-modal-is-open')) {
180                const modalBody = this.modalContainer.querySelector('.aca-modal-body');
181                modalBody.innerHTML = '';
182            }
183        }, 300); // Match CSS animation duration
184
185        this.activeModal = null;
186
187        // Trigger custom event
188        const event = new CustomEvent('aca:modal:closed');
189        document.dispatchEvent(event);
190    }
191
192    /**
193     * Traps focus within the modal
194     * @param {KeyboardEvent} e - The keyboard event
195     */
196    trapFocus(e) {
197        const focusableElements = this.modalContainer.querySelectorAll(
198            'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
199        );
200
201        const firstElement = focusableElements[0];
202        const lastElement = focusableElements[focusableElements.length - 1];
203
204        if (e.shiftKey) {
205            if (document.activeElement === firstElement) {
206                lastElement.focus();
207                e.preventDefault();
208            }
209        } else {
210            if (document.activeElement === lastElement) {
211                firstElement.focus();
212                e.preventDefault();
213            }
214        }
215    }
216
217    /**
218     * Public method to programmatically open a modal
219     * @param {string} modalId - The ID of the modal content to open
220     */
221    open(modalId) {
222        const trigger = document.createElement('div');
223        trigger.setAttribute('data-modal', modalId);
224        this.openModal(trigger);
225    }
226
227    /**
228     * Public method to programmatically close the modal
229     */
230    close() {
231        this.closeModal();
232    }
233}
234
235// Initialize modal system when DOM is ready
236document.addEventListener('DOMContentLoaded', () => {
237    window.acaModal = new ACAModal();
238});
239
240// Also expose the class for manual instantiation
241window.ACAModal = ACAModal;

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.