PageSourceSearch

https://www.aragon.es/o/aragon-theme/js/aria/dialog.js

js aragon.es collected 2026-09-24 08:28:15 UTC 12,218 bytes, 340 lines download raw bytes

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.