PageSourceSearch

https://mairie-grazac31.fr/plugins/cms/resources/js/AmetysFront/Dialog.js

js mairie-grazac31.fr collected 2026-10-03 02:48:27 UTC 10,034 bytes, 266 lines download raw bytes

1/*
2 *  Copyright 2026 Anyware Services
3 *
4 *  Licensed under the Apache License, Version 2.0 (the "License");
5 *  you may not use this file except in compliance with the License.
6 *  You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 *  Unless required by applicable law or agreed to in writing, software
11 *  distributed under the License is distributed on an "AS IS" BASIS,
12 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 *  See the License for the specific language governing permissions and
14 *  limitations under the License.
15 */
16
17/**
18 * AmetysFront.Dialog provides methods to initialize, open and close modal dialogs with ARIA attributes and accessibility features.
19 * Usage:
20 * 1. Prepare your dialog HTML structure with an element having a unique id, and optionally a title element with data-ametys-dialog-title
21 *   <div id="my-dialog" class="my-dialog">
22 *    <h2 data-ametys-dialog-title>My Dialog Title</h2>
23 *    <p>Dialog content goes here...</p>
24 *  </div>
25 *  <script>
26 * 		AmetysFront.Dialog.initialize('my-dialog', {/* options * /});
27 *  </script>
28 * Call AmetysFront.Dialog.initialize('my-dialog') just after in the DOM to prepare the dialog (add ARIA attributes, insert overlay, etc.) and avoid blinking.
29 * You can also pass options to customize behavior and callbacks during initialization, such as onBeforeShow, onShow, onClose, and cssClassPrefix.
30 * 2. Call AmetysFront.Dialog.open('my-dialog') when you want to show the dialog. You can also pass options to customize behavior and callbacks.
31 * 3. The dialog can be closed by clicking on elements with data-ametys-dialog-close attribute or by pressing the Escape key. You can also call AmetysFront.Dialog.close('my-dialog') programmatically.
32 */
33
34AmetysFront.Dialog = {
35    
36	/**
37	 * Initialize a modal dialog with the given id by adding necessary ARIA attributes and inserting an overlay element if not already present. 
38	 * This method does not open the dialog, it only prepares it for opening. You can call this method on page load for all your dialogs to prepare them, and then call open() when you want to show them.
39	 * @param {String} selectorId The id of the modal element to open
40	 * @param {Object} [options] an object with the following optional properties:
41	 * @param {Function} [options.onBeforeShow] callback function to call before showing the modal, with signature (dialogEl)
42	 * @param {Function} [options.onShow] callback function to call after showing the modal, with signature (dialogEl)
43	 * @param {Function} [options.onClose] callback function to call after closing the modal, with signature (dialogEl, externalInvoker)
44	 * @param {String} [options.cssClassPrefix='ametys-dialog'] the CSS class prefix to use for the dialog elements (main dialog, overlay, content)
45	 * @returns {Element} the dialog element, or null if not found
46     */
47	initialize: function(selectorId, options = {})
48	{
49		const dialogEl = document.getElementById(selectorId);
50		
51		if (dialogEl == null)
52		{
53			console.warn(`Dialog element with id "${selectorId}" not found.`);
54			return null;
55		}
56		
57		AmetysFront.Dialog._initializeDialog(dialogEl, options);
58		
59		dialogEl._initOptions = options;
60        
61        return dialogEl;
62	},
63	
64    /**
65     * Open a modal dialog with the given id and set up listeners to close it when clicking on elements with data-ametys-dialog-close attribute or pressing Escape key
66	 * @param {String|Element} selectorIdOrElement The id of the modal element to close or the modal element itself
67	 * @param {Object} [options] an object with the following optional properties:
68	 * @param {Function} [options.onBeforeShow] callback function to call before showing the modal, with signature (dialogEl)
69	 * @param {Function} [options.onShow] callback function to call after showing the modal, with signature (dialogEl)
70	 * @param {Function} [options.onClose] callback function to call after closing the modal, with signature (dialogEl, externalInvoker)
71	 * @param {Boolean} [options.focusFirstElement=true] set to false to not focus the first focusable element in modal when opening it
72	 * @returns {Element} the opened dialog element, or null if not found
73     */
74    open: function(selectorIdOrElement, options = {})
75    {
76		const dialogEl = typeof selectorIdOrElement === 'string' 
77				       ? document.getElementById(selectorIdOrElement)
78				       : selectorIdOrElement;
79		
80		if (dialogEl == null)
81		{
82			console.warn(`Dialog element "${selectorIdOrElement}" not found.`);
83			return null;
84		}
85		
86		if (!dialogEl._ametysModalInitialized)
87		{
88			console.error(`Dialog element "${selectorIdOrElement}" is not initialized. Please call AmetysFront.Dialog.initialize("${selectorIdOrElement}") before opening it.`);
89			return null;
90		}
91		
92		function closeDialogOnEscape(e)
93		{
94			if (e.key === 'Escape') 
95			{
96				closeDialog();
97			}
98		}
99		
100		function closeDialogOnClick(e)
101	    {
102			if (e && e.target.closest('[data-ametys-dialog-close]')) // Close if click on close button or if close triggered by external invoker (not by click on close button or Escape key)
103			{
104				closeDialog();
105			}
106	    }
107		
108		function closeDialog(externalInvoker = false)
109		{
110			AmetysFront.Dialog._closeDialog(dialogEl, externalInvoker);
111		}
112		
113		// Store invoker element reference on the dialog element for later use on close
114		dialogEl._invokerEl = options.invokerEl || document.activeElement;
115		
116		// Store open options
117		dialogEl._openOptions = options;
118		
119		// Store listener references on the dialog element
120		dialogEl._ametysCloseDialog = closeDialog;
121		dialogEl._ametysCloseDialogOnClick = closeDialogOnClick;
122		dialogEl._ametysCloseDialogOnEscape = closeDialogOnEscape;
123		
124		// Add listeners to close event dialog       
125		dialogEl.addEventListener('click', closeDialogOnClick);
126		dialogEl.addEventListener('keydown', closeDialogOnEscape);
127
128		// Show dialog
129		AmetysFront.Dialog._openDialog(dialogEl);
130
131		return dialogEl;
132    },
133	
134	/**
135	 * Close a modal dialog
136	 * @param {String|Element} selectorIdOrElement The id of the modal element to close or the modal element itself
137	 */
138	close: function(selectorIdOrElement)
139	{
140		const dialogEl = typeof selectorIdOrElement === 'string' 
141		       ? document.getElementById(selectorIdOrElement)
142		       : selectorIdOrElement;
143			   
144		if (dialogEl == null) return;
145
146		dialogEl._ametysCloseDialog(true); // Pass true to indicate that the close was triggered by an external invoker (not by click on close button or Escape key)
147	},
148	
149	/**
150	 * @private
151	 */
152	_initializeDialog: function(dialogEl, options = {})
153	{
154		if (dialogEl._ametysModalInitialized !== 'true') 
155		{
156			const cssClassPrefix = options.cssClassPrefix || 'ametys-dialog';
157			dialogEl.classList.add(cssClassPrefix);
158			dialogEl.setAttribute('role', 'dialog');
159			dialogEl.setAttribute('aria-modal', 'true');
160			dialogEl.setAttribute('aria-hidden', 'false');
161			
162			// Insert role document and move modal content inside it
163			const contentEl = document.createElement('div');
164			contentEl.classList.add(cssClassPrefix + '-document');
165			contentEl.append(...dialogEl.childNodes);
166			contentEl.setAttribute('role', 'document');
167			dialogEl.appendChild(contentEl);
168
169			// Insert overlay at first level of modal
170			const overlay = document.createElement('div');
171			overlay.classList.add(cssClassPrefix + '-overlay');
172			overlay.setAttribute('data-ametys-dialog-close', 'true');
173			
174			dialogEl.insertBefore(overlay, dialogEl.firstChild);
175			
176			// Find title
177			const titleEl = dialogEl.querySelector('[data-ametys-dialog-title]');
178			if (titleEl)
179			{
180				titleEl.id = titleEl.id || `ametys-dialog-title-${Math.random().toString(36).substr(2, 9)}`;
181				dialogEl.setAttribute('aria-labelledby', titleEl.id);
182			}
183			
184			// Move dialog element to the end of body to avoid z-index issues and ensure it is above all other content
185			document.body.appendChild(dialogEl);
186					
187			// Mark dialog as initialized
188			dialogEl._ametysModalInitialized = 'true';
189		}
190	},
191	
192	/**
193	 * @private
194	 */
195	_openDialog: function(dialogEl)
196	{
197		if (dialogEl._initOptions.onBeforeShow)
198		{
199			if (dialogEl._initOptions.onBeforeShow(dialogEl) === false) return; // If onBeforeShow returns false, do not open the dialog
200		}
201		
202		if (dialogEl._openOptions.onBeforeShow)
203		{
204			if (dialogEl._openOptions.onBeforeShow(dialogEl) === false) return; // If onBeforeShow returns false, do not open the dialog
205		}
206
207		// Open modal
208		dialogEl.classList.add('ametys-dialog-open');
209		dialogEl.setAttribute('aria-hidden', 'false');
210
211		// Trap focus inside the modal for accessibility
212		AmetysFront.Accessibility.trapFocus(dialogEl, dialogEl._openOptions.focusFirstElement !== false);
213
214		if (dialogEl._initOptions.onShow)
215		{
216			dialogEl._initOptions.onShow(dialogEl);
217		}
218
219		if (dialogEl._openOptions.onShow)
220		{
221			dialogEl._openOptions.onShow(dialogEl);
222		}
223	},
224    
225	/**
226	 * @private
227	 */
228    _closeDialog: function(dialogEl, externalInvoker = false)
229    {
230		dialogEl.classList.remove('ametys-dialog-open');
231        dialogEl.setAttribute('aria-hidden', 'true');
232		
233		dialogEl._invokerEl.focus();
234
235		if (dialogEl._initOptions.onClose)
236		{
237			dialogEl._initOptions.onClose(dialogEl, externalInvoker);
238		}
239		
240		if (dialogEl._openOptions.onClose)
241		{
242			dialogEl._openOptions.onClose(dialogEl, externalInvoker);
243		}
244        
245        // Untrap focus inside the modal for accessibility
246        AmetysFront.Accessibility.untrapFocus(dialogEl);
247		
248		// Remove listeners using stored references
249	    if (dialogEl._ametysCloseDialogOnClick)
250	    {
251	    	dialogEl.removeEventListener('click', dialogEl._ametysCloseDialogOnClick);
252	    }
253
254	    if (dialogEl._ametysCloseDialogOnEscape)
255	    {
256	        dialogEl.removeEventListener('keydown', dialogEl._ametysCloseDialogOnEscape);
257	    }
258	    
259	    // Clean up stored references
260		delete dialogEl._ametysCloseDialog;
261	    delete dialogEl._ametysCloseDialogOnClick;
262	    delete dialogEl._ametysCloseDialogOnEscape;
263		delete dialogEl._invokerEl;
264		delete dialogEl._openOptions;
265    }
266}

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.