PageSourceSearch

https://www.edcu.com/home/diFiles/skins/default/js/a11y.js

js edcu.com collected 2026-10-02 15:35:25 UTC 12,513 bytes, 407 lines download raw bytes

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.