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.