Upgraded drupal core with security updates
[yaffs-website] / web / core / misc / tabbingmanager.js
1 /**
2  * @file
3  * Manages page tabbing modifications made by modules.
4  */
5
6 /**
7  * Allow modules to respond to the constrain event.
8  *
9  * @event drupalTabbingConstrained
10  */
11
12 /**
13  * Allow modules to respond to the tabbingContext release event.
14  *
15  * @event drupalTabbingContextReleased
16  */
17
18 /**
19  * Allow modules to respond to the constrain event.
20  *
21  * @event drupalTabbingContextActivated
22  */
23
24 /**
25  * Allow modules to respond to the constrain event.
26  *
27  * @event drupalTabbingContextDeactivated
28  */
29
30 (function ($, Drupal) {
31
32   'use strict';
33
34   /**
35    * Provides an API for managing page tabbing order modifications.
36    *
37    * @constructor Drupal~TabbingManager
38    */
39   function TabbingManager() {
40
41     /**
42      * Tabbing sets are stored as a stack. The active set is at the top of the
43      * stack. We use a JavaScript array as if it were a stack; we consider the
44      * first element to be the bottom and the last element to be the top. This
45      * allows us to use JavaScript's built-in Array.push() and Array.pop()
46      * methods.
47      *
48      * @type {Array.<Drupal~TabbingContext>}
49      */
50     this.stack = [];
51   }
52
53   /**
54    * Add public methods to the TabbingManager class.
55    */
56   $.extend(TabbingManager.prototype, /** @lends Drupal~TabbingManager# */{
57
58     /**
59      * Constrain tabbing to the specified set of elements only.
60      *
61      * Makes elements outside of the specified set of elements unreachable via
62      * the tab key.
63      *
64      * @param {jQuery} elements
65      *   The set of elements to which tabbing should be constrained. Can also
66      *   be a jQuery-compatible selector string.
67      *
68      * @return {Drupal~TabbingContext}
69      *   The TabbingContext instance.
70      *
71      * @fires event:drupalTabbingConstrained
72      */
73     constrain: function (elements) {
74       // Deactivate all tabbingContexts to prepare for the new constraint. A
75       // tabbingContext instance will only be reactivated if the stack is
76       // unwound to it in the _unwindStack() method.
77       var il = this.stack.length;
78       for (var i = 0; i < il; i++) {
79         this.stack[i].deactivate();
80       }
81
82       // The "active tabbing set" are the elements tabbing should be constrained
83       // to.
84       var $elements = $(elements).find(':tabbable').addBack(':tabbable');
85
86       var tabbingContext = new TabbingContext({
87         // The level is the current height of the stack before this new
88         // tabbingContext is pushed on top of the stack.
89         level: this.stack.length,
90         $tabbableElements: $elements
91       });
92
93       this.stack.push(tabbingContext);
94
95       // Activates the tabbingContext; this will manipulate the DOM to constrain
96       // tabbing.
97       tabbingContext.activate();
98
99       // Allow modules to respond to the constrain event.
100       $(document).trigger('drupalTabbingConstrained', tabbingContext);
101
102       return tabbingContext;
103     },
104
105     /**
106      * Restores a former tabbingContext when an active one is released.
107      *
108      * The TabbingManager stack of tabbingContext instances will be unwound
109      * from the top-most released tabbingContext down to the first non-released
110      * tabbingContext instance. This non-released instance is then activated.
111      */
112     release: function () {
113       // Unwind as far as possible: find the topmost non-released
114       // tabbingContext.
115       var toActivate = this.stack.length - 1;
116       while (toActivate >= 0 && this.stack[toActivate].released) {
117         toActivate--;
118       }
119
120       // Delete all tabbingContexts after the to be activated one. They have
121       // already been deactivated, so their effect on the DOM has been reversed.
122       this.stack.splice(toActivate + 1);
123
124       // Get topmost tabbingContext, if one exists, and activate it.
125       if (toActivate >= 0) {
126         this.stack[toActivate].activate();
127       }
128     },
129
130     /**
131      * Makes all elements outside of the tabbingContext's set untabbable.
132      *
133      * Elements made untabbable have their original tabindex and autofocus
134      * values stored so that they might be restored later when this
135      * tabbingContext is deactivated.
136      *
137      * @param {Drupal~TabbingContext} tabbingContext
138      *   The TabbingContext instance that has been activated.
139      */
140     activate: function (tabbingContext) {
141       var $set = tabbingContext.$tabbableElements;
142       var level = tabbingContext.level;
143       // Determine which elements are reachable via tabbing by default.
144       var $disabledSet = $(':tabbable')
145         // Exclude elements of the active tabbing set.
146         .not($set);
147       // Set the disabled set on the tabbingContext.
148       tabbingContext.$disabledElements = $disabledSet;
149       // Record the tabindex for each element, so we can restore it later.
150       var il = $disabledSet.length;
151       for (var i = 0; i < il; i++) {
152         this.recordTabindex($disabledSet.eq(i), level);
153       }
154       // Make all tabbable elements outside of the active tabbing set
155       // unreachable.
156       $disabledSet
157         .prop('tabindex', -1)
158         .prop('autofocus', false);
159
160       // Set focus on an element in the tabbingContext's set of tabbable
161       // elements. First, check if there is an element with an autofocus
162       // attribute. Select the last one from the DOM order.
163       var $hasFocus = $set.filter('[autofocus]').eq(-1);
164       // If no element in the tabbable set has an autofocus attribute, select
165       // the first element in the set.
166       if ($hasFocus.length === 0) {
167         $hasFocus = $set.eq(0);
168       }
169       $hasFocus.trigger('focus');
170     },
171
172     /**
173      * Restores that tabbable state of a tabbingContext's disabled elements.
174      *
175      * Elements that were made untabbable have their original tabindex and
176      * autofocus values restored.
177      *
178      * @param {Drupal~TabbingContext} tabbingContext
179      *   The TabbingContext instance that has been deactivated.
180      */
181     deactivate: function (tabbingContext) {
182       var $set = tabbingContext.$disabledElements;
183       var level = tabbingContext.level;
184       var il = $set.length;
185       for (var i = 0; i < il; i++) {
186         this.restoreTabindex($set.eq(i), level);
187       }
188     },
189
190     /**
191      * Records the tabindex and autofocus values of an untabbable element.
192      *
193      * @param {jQuery} $el
194      *   The set of elements that have been disabled.
195      * @param {number} level
196      *   The stack level for which the tabindex attribute should be recorded.
197      */
198     recordTabindex: function ($el, level) {
199       var tabInfo = $el.data('drupalOriginalTabIndices') || {};
200       tabInfo[level] = {
201         tabindex: $el[0].getAttribute('tabindex'),
202         autofocus: $el[0].hasAttribute('autofocus')
203       };
204       $el.data('drupalOriginalTabIndices', tabInfo);
205     },
206
207     /**
208      * Restores the tabindex and autofocus values of a reactivated element.
209      *
210      * @param {jQuery} $el
211      *   The element that is being reactivated.
212      * @param {number} level
213      *   The stack level for which the tabindex attribute should be restored.
214      */
215     restoreTabindex: function ($el, level) {
216       var tabInfo = $el.data('drupalOriginalTabIndices');
217       if (tabInfo && tabInfo[level]) {
218         var data = tabInfo[level];
219         if (data.tabindex) {
220           $el[0].setAttribute('tabindex', data.tabindex);
221         }
222         // If the element did not have a tabindex at this stack level then
223         // remove it.
224         else {
225           $el[0].removeAttribute('tabindex');
226         }
227         if (data.autofocus) {
228           $el[0].setAttribute('autofocus', 'autofocus');
229         }
230
231         // Clean up $.data.
232         if (level === 0) {
233           // Remove all data.
234           $el.removeData('drupalOriginalTabIndices');
235         }
236         else {
237           // Remove the data for this stack level and higher.
238           var levelToDelete = level;
239           while (tabInfo.hasOwnProperty(levelToDelete)) {
240             delete tabInfo[levelToDelete];
241             levelToDelete++;
242           }
243           $el.data('drupalOriginalTabIndices', tabInfo);
244         }
245       }
246     }
247   });
248
249   /**
250    * Stores a set of tabbable elements.
251    *
252    * This constraint can be removed with the release() method.
253    *
254    * @constructor Drupal~TabbingContext
255    *
256    * @param {object} options
257    *   A set of initiating values
258    * @param {number} options.level
259    *   The level in the TabbingManager's stack of this tabbingContext.
260    * @param {jQuery} options.$tabbableElements
261    *   The DOM elements that should be reachable via the tab key when this
262    *   tabbingContext is active.
263    * @param {jQuery} options.$disabledElements
264    *   The DOM elements that should not be reachable via the tab key when this
265    *   tabbingContext is active.
266    * @param {bool} options.released
267    *   A released tabbingContext can never be activated again. It will be
268    *   cleaned up when the TabbingManager unwinds its stack.
269    * @param {bool} options.active
270    *   When true, the tabbable elements of this tabbingContext will be reachable
271    *   via the tab key and the disabled elements will not. Only one
272    *   tabbingContext can be active at a time.
273    */
274   function TabbingContext(options) {
275
276     $.extend(this, /** @lends Drupal~TabbingContext# */{
277
278       /**
279        * @type {?number}
280        */
281       level: null,
282
283       /**
284        * @type {jQuery}
285        */
286       $tabbableElements: $(),
287
288       /**
289        * @type {jQuery}
290        */
291       $disabledElements: $(),
292
293       /**
294        * @type {bool}
295        */
296       released: false,
297
298       /**
299        * @type {bool}
300        */
301       active: false
302     }, options);
303   }
304
305   /**
306    * Add public methods to the TabbingContext class.
307    */
308   $.extend(TabbingContext.prototype, /** @lends Drupal~TabbingContext# */{
309
310     /**
311      * Releases this TabbingContext.
312      *
313      * Once a TabbingContext object is released, it can never be activated
314      * again.
315      *
316      * @fires event:drupalTabbingContextReleased
317      */
318     release: function () {
319       if (!this.released) {
320         this.deactivate();
321         this.released = true;
322         Drupal.tabbingManager.release(this);
323         // Allow modules to respond to the tabbingContext release event.
324         $(document).trigger('drupalTabbingContextReleased', this);
325       }
326     },
327
328     /**
329      * Activates this TabbingContext.
330      *
331      * @fires event:drupalTabbingContextActivated
332      */
333     activate: function () {
334       // A released TabbingContext object can never be activated again.
335       if (!this.active && !this.released) {
336         this.active = true;
337         Drupal.tabbingManager.activate(this);
338         // Allow modules to respond to the constrain event.
339         $(document).trigger('drupalTabbingContextActivated', this);
340       }
341     },
342
343     /**
344      * Deactivates this TabbingContext.
345      *
346      * @fires event:drupalTabbingContextDeactivated
347      */
348     deactivate: function () {
349       if (this.active) {
350         this.active = false;
351         Drupal.tabbingManager.deactivate(this);
352         // Allow modules to respond to the constrain event.
353         $(document).trigger('drupalTabbingContextDeactivated', this);
354       }
355     }
356   });
357
358   // Mark this behavior as processed on the first pass and return if it is
359   // already processed.
360   if (Drupal.tabbingManager) {
361     return;
362   }
363
364   /**
365    * @type {Drupal~TabbingManager}
366    */
367   Drupal.tabbingManager = new TabbingManager();
368
369 }(jQuery, Drupal));