1/* global NodeList, Element, define */ 2 3(function (global) { 4 'use strict'; 5 6 var FOCUSABLE_ELEMENTS = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'iframe', 'object', 'embed', '[contenteditable]', '[tabindex]:not([tabindex^="-"])']; 7 var TAB_KEY = 9; 8 var ESCAPE_KEY = 27; 9 var focusedBeforeDialog; 10 11 /** 12 * Define the constructor to instantiate a dialog 13 * 14 * @constructor 15 * @param {Element} node 16 * @param {(NodeList | Element | string)} targets 17 */ 18 function A11yDialog (node, targets) { 19 // Prebind the functions that will be bound in addEventListener and 20 // removeEventListener to avoid losing references 21 this._show = this.show.bind(this); 22 this._hide = this.hide.bind(this); 23 this._maintainFocus = this._maintainFocus.bind(this); 24 this._bindKeypress = this._bindKeypress.bind(this); 25 26 // Keep a reference of the node on the instance 27 this.node = node; 28 29 // Keep an object of listener types mapped to callback functions 30 this._listeners = {}; 31 32 // Initialise everything needed for the dialog to work properly 33 this.create(targets); 34 } 35 36 /** 37 * Set up everything necessary for the dialog to be functioning 38 * 39 * @param {(NodeList | Element | string)} targets 40 * @return {this} 41 */ 42 A11yDialog.prototype.create = function (targets) { 43 // Keep a collection of nodes to disable/enable when toggling the dialog 44 this._targets = this._targets || collect(targets) || getSiblings(this.node); 45 46 // Make sure the dialog element is disabled on load, and that the `shown` 47 // property is synced with its value 48 this.node.setAttribute('aria-hidden', true); 49 this.shown = false; 50 51 // Keep a collection of dialog openers, each of which will be bound a click 52 // event listener to open the dialog 53 this._openers = $$('[data-a11y-dialog-show="' + this.node.id + '"]'); 54 this._openers.forEach(function (opener) { 55 opener.addEventListener('click', this._show); 56 }.bind(this)); 57 58 // Keep a collection of dialog closers, each of which will be bound a click 59 // event listener to close the dialog 60 this._closers = $$('[data-a11y-dialog-hide]', this.node) 61 .concat($$('[data-a11y-dialog-hide="' + this.node.id + '"]')); 62 this._closers.forEach(function (closer) { 63 closer.addEventListener('click', this._hide); 64 }.bind(this)); 65 66 // Execute all callbacks registered for the `create` event 67 this._fire('create'); 68 69 return this; 70 }; 71 72 /** 73 * Show the dialog element, disable all the targets (siblings), trap the 74 * current focus within it, listen for some specific key presses and fire all 75 * registered callbacks for `show` event 76 * 77 * @param {Event} event 78 * @return {this} 79 */ 80 A11yDialog.prototype.show = function (event) { 81 // If the dialog is already open, abort 82 if (this.shown) { 83 return this; 84 } 85 86 this.shown = true; 87 this.node.removeAttribute('aria-hidden'); 88 89 // Iterate over the targets to disable them by setting their `aria-hidden` 90 // attribute to `true`; in case they already have this attribute, keep a 91 // reference of their original value to be able to restore it later 92 this._targets.forEach(function (target) { 93 var original = target.getAttribute('aria-hidden'); 94 95 if (original) { 96 target.setAttribute('data-a11y-dialog-original', original); 97 } 98 99 target.setAttribute('aria-hidden', 'true'); 100 }); 101 102 // Keep a reference to the currently focused element to be able to restore 103 // it later, then set the focus to the first focusable child of the dialog 104 // element 105 focusedBeforeDialog = document.activeElement; 106 setFocusToFirstItem(this.node); 107 108 // Bind a focus event listener to the body element to make sure the focus 109 // stays trapped inside the dialog while open, and start listening for some 110 // specific key presses (TAB and ESC) 111 document.body.addEventListener('focus', this._maintainFocus, true); 112 document.addEventListener('keydown', this._bindKeypress); 113 114 // Execute all callbacks registered for the `show` event 115 this._fire('show', event); 116 117 return this; 118 }; 119 120 /** 121 * Hide the dialog element, enable all the targets (siblings), restore the 122 * focus to the previously active element, stop listening for some specific 123 * key presses and fire all registered callbacks for `hide` event 124 * 125 * @param {Event} event 126 * @return {this} 127 */ 128 A11yDialog.prototype.hide = function (event) { 129 // If the dialog is already closed, abort 130 if (!this.shown) { 131 return this; 132 } 133 134 this.shown = false;
135 this.node.setAttribute('aria-hidden', 'true'); 136 137 // Iterate over the targets to enable them by remove their `aria-hidden` 138 // attribute or resetting them to their initial value 139 this._targets.forEach(function (target) { 140 var original = target.getAttribute('data-a11y-dialog-original'); 141 142 if (original) { 143 target.setAttribute('aria-hidden', original); 144 target.removeAttribute('data-a11y-dialog-original'); 145 } else { 146 target.removeAttribute('aria-hidden'); 147 } 148 }); 149 150 // If their was a focused element before the dialog was opened, restore the 151 // focus back to it 152 if (focusedBeforeDialog) { 153 focusedBeforeDialog.focus(); 154 } 155 156 // Remove the focus event listener to the body element and stop listening 157 // for specific key presses 158 document.body.removeEventListener('focus', this._maintainFocus, true); 159 document.removeEventListener('keydown', this._bindKeypress); 160 161 // Execute all callbacks registered for the `hide` event 162 this._fire('hide', event); 163 164 return this; 165 }; 166 167 /** 168 * Destroy the current instance (after making sure the dialog has been hidden) 169 * and remove all associated listeners from dialog openers and closers 170 * 171 * @return {this} 172 */ 173 A11yDialog.prototype.destroy = function () { 174 // Hide the dialog to avoid destroying an open instance 175 this.hide(); 176 177 // Remove the click event listener from all dialog openers 178 this._openers.forEach(function (opener) { 179 opener.removeEventListener('click', this._show); 180 }.bind(this)); 181 182 // Remove the click event listener from all dialog closers 183 this._closers.forEach(function (closer) { 184 closer.removeEventListener('click', this._hide); 185 }.bind(this)); 186 187 // Execute all callbacks registered for the `destroy` event 188 this._fire('destroy'); 189 190 // Keep an object of listener types mapped to callback functions 191 this._listeners = {}; 192 193 return this; 194 }; 195 196 /** 197 * Register a new callback for the given event type 198 * 199 * @param {string} type 200 * @param {Function} handler 201 */ 202 A11yDialog.prototype.on = function (type, handler) { 203 if (typeof this._listeners[type] === 'undefined') { 204 this._listeners[type] = []; 205 } 206 207 this._listeners[type].push(handler); 208 209 return this; 210 }; 211 212 /** 213 * Unregister an existing callback for the given event type 214 * 215 * @param {string} type 216 * @param {Function} handler 217 */ 218 A11yDialog.prototype.off = function (type, handler) { 219 var index = this._listeners[type].indexOf(handler); 220 221 if (index > -1) { 222 this._listeners[type].splice(index, 1); 223 } 224 225 return this; 226 }; 227 228 /** 229 * Iterate over all registered handlers for given type and call them all with 230 * the dialog element as first argument, trigger (opener / closer) as second 231 * argument, if any. 232 * 233 * @access private 234 * @param {string} type 235 * @param {Event} event 236 */ 237 A11yDialog.prototype._fire = function (type, event) { 238 var listeners = this._listeners[type] || []; 239 var trigger = event ? event.target : void 0; 240 241 listeners.forEach(function (listener) { 242 listener(this.node, trigger); 243 }.bind(this)); 244 }; 245 246 /** 247 * Private event handler used when listening to some specific key presses 248 * (namely ESCAPE and TAB) 249 * 250 * @access private 251 * @param {Event} event 252 */ 253 A11yDialog.prototype._bindKeypress = function (event) { 254 // If the dialog is shown and the ESCAPE key is being pressed, prevent any 255 // further effects from the ESCAPE key and hide the dialog 256 if (this.shown && event.which === ESCAPE_KEY) { 257 //console.log(this.id); 258 //this.classList.toggle("in"); 259 event.preventDefault(); 260 this.hide(); 261 } 262 263 // If the dialog is shown and the TAB key is being pressed, make sure the 264 // focus stays trapped within the dialog element 265 if (this.shown && event.which === TAB_KEY) { 266 trapTabKey(this.node, event); 267 } 268 }; 269 270 /** 271 * Private event handler used when making sure the focus stays within the 272 * currently open dialog 273 * 274 * @access private 275 * @param {Event} event 276 */ 277 A11yDialog.prototype._maintainFocus = function (event) { 278 // If the dialog is shown and the focus is not within the dialog element, 279 // move it back to its first focusable child 280 if (this.shown && !this.node.contains(event.target)) { 281 setFocusToFirstItem(this.node); 282 } 283 }; 284 285 /** 286 * Convert a NodeList into an array 287 * 288 * @param {NodeList} collection 289 * @return {Array<Element>} 290 */ 291 function toArray (collection) { 292 return Array.prototype.slice.call(collection); 293 } 294 295 /** 296 * Query the DOM for nodes matching the given selector, scoped to context (or 297 * the whole document) 298 * 299 * @param {String} selector 300 * @param {Element} [context = document] 301 * @return {Array<Element>} 302 */ 303 function $$ (selector, context) { 304 return toArray((context || document).querySelectorAll(selector)); 305 } 306 307 /** 308 * Return an array of Element based on given argument (NodeList, Element or 309 * string representing a selector) 310 * 311 * @param {(NodeList | Element | string)} target 312 * @return {Array<Element>} 313 */ 314 function collect (target) { 315 if (NodeList.prototype.isPrototypeOf(target)) { 316 return toArray(target); 317 } 318 319 if (Element.prototype.isPrototypeOf(target)) { 320 return [target]; 321 } 322 323 if (typeof target === 'string') { 324 return $$(target); 325 } 326 } 327 328 /** 329 * Set the focus to the first focusable child of the given element 330 * 331 * @param {Element} node 332 */ 333 function setFocusToFirstItem (node) { 334 var focusableChildren = getFocusableChildren(node); 335 336 if (focusableChildren.length) { 337 focusableChildren[0].focus(); 338 } 339 } 340 341 /** 342 * Get the focusable children of the given element 343 * 344 * @param {Element} node 345 * @return {Array<Element>} 346 */ 347 function getFocusableChildren (node) { 348 return $$(FOCUSABLE_ELEMENTS.join(','), node).filter(function (child) { 349 return !!(child.offsetWidth || child.offsetHeight || child.getClientRects().length); 350 }); 351 } 352 353 /** 354 * Trap the focus inside the given element 355 * 356 * @param {Element} node 357 * @param {Event} event 358 */ 359 function trapTabKey (node, event) { 360 var focusableChildren = getFocusableChildren(node); 361 var focusedItemIndex = focusableChildren.indexOf(document.activeElement); 362 363 // If the SHIFT key is being pressed while tabbing (moving backwards) and 364 // the currently focused item is the first one, move the focus to the last 365 // focusable item from the dialog element 366 if (event.shiftKey && focusedItemIndex === 0) { 367 focusableChildren[focusableChildren.length - 1].focus(); 368 event.preventDefault(); 369 // If the SHIFT key is not being pressed (moving forwards) and the currently 370 // focused item is the last one, move the focus to the first focusable item 371 // from the dialog element 372 } else if (!event.shiftKey && focusedItemIndex === focusableChildren.length - 1) { 373 focusableChildren[0].focus(); 374 event.preventDefault(); 375 } 376 } 377 378 /** 379 * Retrieve siblings from given element 380 * 381 * @param {Element} node 382 * @return {Array<Element>} 383 */ 384 function getSiblings (node) { 385 var nodes = toArray(node.parentNode.childNodes); 386 var siblings = nodes.filter(function (node) { 387 return node.nodeType === 1; 388 }); 389 390 siblings.splice(siblings.indexOf(node), 1); 391 392 return siblings; 393 } 394 395 if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 396 module.exports = A11yDialog; 397 } else if (typeof define === 'function' && define.amd) { 398 define('A11yDialog', [], function () { 399 return A11yDialog; 400 }); 401 } else if (typeof global === 'object') { 402 global.A11yDialog = A11yDialog; 403 } 404}(typeof global !== 'undefined' ? global : window)); 405 406 407
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.