1export function dialog(aria) { 2 /* 3 * This content is licensed according to the W3C Software License at 4 * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document 5 */ 6 7 (function () { 8 /* 9 * When util functions move focus around, set this true so the focus listener 10 * can ignore the events. 11 */ 12 aria.Utils.IgnoreUtilFocusChanges = false; 13 14 aria.Utils.dialogOpenClass = 'has-dialog'; 15 16 /** 17 * @desc Set focus on descendant nodes until the first focusable element is 18 * found. 19 * @param element 20 * DOM node for which to find the first focusable descendant. 21 * @returns 22 * true if a focusable element is found and focus is set. 23 */ 24 aria.Utils.focusFirstDescendant = function (element) { 25 for (var i = 0; i < element.childNodes.length; i++) { 26 var child = element.childNodes[i]; 27 if (aria.Utils.attemptFocus(child) || 28 aria.Utils.focusFirstDescendant(child)) { 29 return true; 30 } 31 } 32 return false; 33 }; // end focusFirstDescendant 34 35 /** 36 * @desc Find the last descendant node that is focusable. 37 * @param element 38 * DOM node for which to find the last focusable descendant. 39 * @returns 40 * true if a focusable element is found and focus is set. 41 */ 42 aria.Utils.focusLastDescendant = function (element) { 43 for (var i = element.childNodes.length - 1; i >= 0; i--) { 44 var child = element.childNodes[i]; 45 if (aria.Utils.attemptFocus(child) || 46 aria.Utils.focusLastDescendant(child)) { 47 return true; 48 } 49 } 50 return false; 51 }; // end focusLastDescendant 52 53 /** 54 * @desc Set Attempt to set focus on the current node. 55 * @param element 56 * The node to attempt to focus on. 57 * @returns 58 * true if element is focused. 59 */ 60 aria.Utils.attemptFocus = function (element) { 61 if (!aria.Utils.isFocusable(element)) { 62 return false; 63 } 64 65 aria.Utils.IgnoreUtilFocusChanges = true; 66 try { 67 element.focus(); 68 } 69 catch (e) { 70 } 71 aria.Utils.IgnoreUtilFocusChanges = false; 72 return (document.activeElement === element); 73 }; // end attemptFocus 74 75 /* Modals can open modals. Keep track of them with this array. */ 76 aria.OpenDialogList = aria.OpenDialogList || new Array(0); 77 78 /** 79 * @returns the last opened dialog (the current dialog) 80 */ 81 aria.getCurrentDialog = function () { 82 if (aria.OpenDialogList && aria.OpenDialogList.length) { 83 return aria.OpenDialogList[aria.OpenDialogList.length - 1]; 84 } 85 }; 86 87 aria.closeCurrentDialog = function () { 88 var currentDialog = aria.getCurrentDialog(); 89 if (currentDialog) { 90 currentDialog.close(); 91 return true; 92 } 93 94 return false; 95 }; 96 97 aria.handleEscape = function (event) { 98 var key = event.which || event.keyCode; 99 100 if (key === aria.KeyCode.ESC && aria.closeCurrentDialog()) { 101 event.stopPropagation(); 102 } 103 }; 104 105 document.addEventListener('keyup', aria.handleEscape); 106 107 /** 108 * @constructor 109 * @desc Dialog object providing modal focus management. 110 * 111 * Assumptions: The element serving as the dialog container is present in the 112 * DOM and hidden. The dialog container has role='dialog'. 113 * 114 * @param dialogId 115 * The ID of the element serving as the dialog container. 116 * @param focusAfterClosed 117 * Either the DOM node or the ID of the DOM node to focus when the 118 * dialog closes. 119 * @param focusFirst 120 * Optional parameter containing either the DOM node or the ID of the 121 * DOM node to focus when the dialog opens. If not specified, the 122 * first focusable element in the dialog will receive focus. 123 */ 124 aria.Dialog = function (dialogId, focusAfterClosed, focusFirst) { 125 this.dialogNode = document.getElementById(dialogId); 126 if (this.dialogNode === null) { 127 throw new Error('No element found with id="' + dialogId + '".'); 128 } 129
130 var validRoles = ['dialog', 'alertdialog']; 131 var isDialog = (this.dialogNode.getAttribute('role') || '') 132 .trim() 133 .split(/\s+/g) 134 .some(function (token) { 135 return validRoles.some(function (role) { 136 return token === role; 137 }); 138 }); 139 if (!isDialog) { 140 throw new Error( 141 'Dialog() requires a DOM element with ARIA role of dialog or alertdialog.'); 142 } 143 144 // Wrap in an individual backdrop element if one doesn't exist 145 // Native <dialog> elements use the ::backdrop pseudo-element, which 146 // works similarly. 147 var backdropClass = 'dialog-backdrop'; 148 if (this.dialogNode.parentNode.classList.contains(backdropClass)) { 149 this.backdropNode = this.dialogNode.parentNode; 150 } 151 else { 152 this.backdropNode = document.createElement('div'); 153 this.backdropNode.className = backdropClass; 154 this.dialogNode.parentNode.insertBefore(this.backdropNode, this.dialogNode); 155 this.backdropNode.appendChild(this.dialogNode); 156 } 157 this.backdropNode.classList.add('active'); 158 159 // Disable scroll on the body element 160 document.body.classList.add(aria.Utils.dialogOpenClass); 161 162 if (typeof focusAfterClosed === 'string') { 163 this.focusAfterClosed = document.getElementById(focusAfterClosed); 164 } 165 else if (typeof focusAfterClosed === 'object') { 166 this.focusAfterClosed = focusAfterClosed; 167 } 168 else { 169 throw new Error( 170 'the focusAfterClosed parameter is required for the aria.Dialog constructor.'); 171 } 172 173 if (typeof focusFirst === 'string') { 174 this.focusFirst = document.getElementById(focusFirst); 175 } 176 else if (typeof focusFirst === 'object') { 177 this.focusFirst = focusFirst; 178 } 179 else { 180 this.focusFirst = null; 181 } 182 183 // Bracket the dialog node with two invisible, focusable nodes. 184 // While this dialog is open, we use these to make sure that focus never 185 // leaves the document even if dialogNode is the first or last node. 186 var preDiv = document.createElement('div'); 187 this.preNode = this.dialogNode.parentNode.insertBefore(preDiv, 188 this.dialogNode); 189 this.preNode.tabIndex = 0; 190 var postDiv = document.createElement('div'); 191 this.postNode = this.dialogNode.parentNode.insertBefore(postDiv, 192 this.dialogNode.nextSibling); 193 this.postNode.tabIndex = 0; 194 195 // If this modal is opening on top of one that is already open, 196 // get rid of the document focus listener of the open dialog. 197 if (aria.OpenDialogList.length > 0) { 198 aria.getCurrentDialog().removeListeners(); 199 } 200 201 this.addListeners(); 202 aria.OpenDialogList.push(this); 203 this.clearDialog(); 204 this.dialogNode.className = 'default_dialog'; // make visible 205 206 if (this.focusFirst) { 207 this.focusFirst.focus(); 208 } 209 else { 210 aria.Utils.focusFirstDescendant(this.dialogNode); 211 } 212 213 this.lastFocus = document.activeElement; 214 }; // end Dialog constructor 215 216 aria.Dialog.prototype.clearDialog = function () { 217 Array.prototype.map.call( 218 this.dialogNode.querySelectorAll('input'), 219 function (input) { 220 input.value = ''; 221 } 222 ); 223 }; 224 225 /** 226 * @desc 227 * Hides the current top dialog, 228 * removes listeners of the top dialog, 229 * restore listeners of a parent dialog if one was open under the one that just closed, 230 * and sets focus on the element specified for focusAfterClosed. 231 */ 232 aria.Dialog.prototype.close = function () { 233 aria.OpenDialogList.pop(); 234 this.removeListeners(); 235 aria.Utils.remove(this.preNode); 236 aria.Utils.remove(this.postNode); 237 this.dialogNode.className = 'hidden'; 238 this.backdropNode.classList.remove('active'); 239 this.focusAfterClosed.focus(); 240 241 // If a dialog was open underneath this one, restore its listeners. 242 if (aria.OpenDialogList.length > 0) { 243 aria.getCurrentDialog().addListeners(); 244 } 245 else { 246 document.body.classList.remove(aria.Utils.dialogOpenClass); 247 } 248 }; // end close 249 250 /**
251 * @desc 252 * Hides the current dialog and replaces it with another. 253 * 254 * @param newDialogId 255 * ID of the dialog that will replace the currently open top dialog. 256 * @param newFocusAfterClosed 257 * Optional ID or DOM node specifying where to place focus when the new dialog closes. 258 * If not specified, focus will be placed on the element specified by the dialog being replaced. 259 * @param newFocusFirst 260 * Optional ID or DOM node specifying where to place focus in the new dialog when it opens. 261 * If not specified, the first focusable element will receive focus. 262 */ 263 aria.Dialog.prototype.replace = function (newDialogId, newFocusAfterClosed, 264 newFocusFirst) { 265 var closedDialog = aria.getCurrentDialog(); 266 aria.OpenDialogList.pop(); 267 this.removeListeners(); 268 aria.Utils.remove(this.preNode); 269 aria.Utils.remove(this.postNode); 270 this.dialogNode.className = 'hidden'; 271 this.backdropNode.classList.remove('active'); 272 273 var focusAfterClosed = newFocusAfterClosed || this.focusAfterClosed; 274 var dialog = new aria.Dialog(newDialogId, focusAfterClosed, newFocusFirst); 275 }; // end replace 276 277 aria.Dialog.prototype.addListeners = function () { 278 document.addEventListener('focus', this.trapFocus, true); 279 document.addEventListener('click', this.clickOutSide, true) 280 }; // end addListeners 281 282 aria.Dialog.prototype.removeListeners = function () { 283 document.removeEventListener('focus', this.trapFocus, true); 284 document.removeEventListener('click', this.clickOutSide, true); 285 }; // end removeListeners 286 287 aria.Dialog.prototype.clickOutSide = function (event) { 288 var currentDialog = aria.getCurrentDialog(); 289 var getDialogElement = currentDialog.dialogNode; 290 const isClickInside = getDialogElement.contains(event.target) 291 292 if (!isClickInside) { 293 currentDialog.close(); 294 } 295 }; 296 297 aria.Dialog.prototype.trapFocus = function (event) { 298 if (aria.Utils.IgnoreUtilFocusChanges) { 299 return; 300 } 301 var currentDialog = aria.getCurrentDialog(); 302 if (currentDialog.dialogNode.contains(event.target)) { 303 currentDialog.lastFocus = event.target; 304 } 305 else { 306 aria.Utils.focusFirstDescendant(currentDialog.dialogNode); 307 if (currentDialog.lastFocus == document.activeElement) { 308 aria.Utils.focusLastDescendant(currentDialog.dialogNode); 309 } 310 currentDialog.lastFocus = document.activeElement; 311 } 312 }; // end trapFocus 313 314 window.openDialog = function (dialogId, focusAfterClosed, focusFirst) { 315 const dialogElement = document.getElementById(dialogId); 316 if(dialogElement) { 317 var dialog = new aria.Dialog(dialogId, focusAfterClosed, focusFirst); 318 return [dialogId, focusAfterClosed, focusFirst] 319 } else { 320 console.log('There is no dialog with this id in the document.'); 321 return null; 322 } 323 }; 324 325 window.closeDialog = function (closeButton) { 326 var topDialog = aria.getCurrentDialog(); 327 if (topDialog.dialogNode.contains(closeButton)) { 328 topDialog.close(); 329 } 330 }; // end closeDialog 331 332 window.replaceDialog = function (newDialogId, newFocusAfterClosed, newFocusFirst) { 333 var topDialog = aria.getCurrentDialog(); 334 if (topDialog.dialogNode.contains(document.activeElement)) { 335 topDialog.replace(newDialogId, newFocusAfterClosed, newFocusFirst); 336 } 337 }; // end replaceDialog 338 339 }()); 340}
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.