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