|
1 (function (global, factory) { |
|
2 typeof exports === 'object' && typeof module !== 'undefined' ? factory() : |
|
3 typeof define === 'function' && define.amd ? define('inert', factory) : |
|
4 (factory()); |
|
5 }(this, (function () { 'use strict'; |
|
6 |
|
7 var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); |
|
8 |
|
9 function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } |
|
10 |
|
11 /** |
|
12 * This work is licensed under the W3C Software and Document License |
|
13 * (http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document). |
|
14 */ |
|
15 |
|
16 (function () { |
|
17 // Return early if we're not running inside of the browser. |
|
18 if (typeof window === 'undefined') { |
|
19 return; |
|
20 } |
|
21 |
|
22 // Convenience function for converting NodeLists. |
|
23 /** @type {typeof Array.prototype.slice} */ |
|
24 var slice = Array.prototype.slice; |
|
25 |
|
26 /** |
|
27 * IE has a non-standard name for "matches". |
|
28 * @type {typeof Element.prototype.matches} |
|
29 */ |
|
30 var matches = Element.prototype.matches || Element.prototype.msMatchesSelector; |
|
31 |
|
32 /** @type {string} */ |
|
33 var _focusableElementsString = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'details', 'summary', 'iframe', 'object', 'embed', '[contenteditable]'].join(','); |
|
34 |
|
35 /** |
|
36 * `InertRoot` manages a single inert subtree, i.e. a DOM subtree whose root element has an `inert` |
|
37 * attribute. |
|
38 * |
|
39 * Its main functions are: |
|
40 * |
|
41 * - to create and maintain a set of managed `InertNode`s, including when mutations occur in the |
|
42 * subtree. The `makeSubtreeUnfocusable()` method handles collecting `InertNode`s via registering |
|
43 * each focusable node in the subtree with the singleton `InertManager` which manages all known |
|
44 * focusable nodes within inert subtrees. `InertManager` ensures that a single `InertNode` |
|
45 * instance exists for each focusable node which has at least one inert root as an ancestor. |
|
46 * |
|
47 * - to notify all managed `InertNode`s when this subtree stops being inert (i.e. when the `inert` |
|
48 * attribute is removed from the root node). This is handled in the destructor, which calls the |
|
49 * `deregister` method on `InertManager` for each managed inert node. |
|
50 */ |
|
51 |
|
52 var InertRoot = function () { |
|
53 /** |
|
54 * @param {!HTMLElement} rootElement The HTMLElement at the root of the inert subtree. |
|
55 * @param {!InertManager} inertManager The global singleton InertManager object. |
|
56 */ |
|
57 function InertRoot(rootElement, inertManager) { |
|
58 _classCallCheck(this, InertRoot); |
|
59 |
|
60 /** @type {!InertManager} */ |
|
61 this._inertManager = inertManager; |
|
62 |
|
63 /** @type {!HTMLElement} */ |
|
64 this._rootElement = rootElement; |
|
65 |
|
66 /** |
|
67 * @type {!Set<!InertNode>} |
|
68 * All managed focusable nodes in this InertRoot's subtree. |
|
69 */ |
|
70 this._managedNodes = new Set(); |
|
71 |
|
72 // Make the subtree hidden from assistive technology |
|
73 if (this._rootElement.hasAttribute('aria-hidden')) { |
|
74 /** @type {?string} */ |
|
75 this._savedAriaHidden = this._rootElement.getAttribute('aria-hidden'); |
|
76 } else { |
|
77 this._savedAriaHidden = null; |
|
78 } |
|
79 this._rootElement.setAttribute('aria-hidden', 'true'); |
|
80 |
|
81 // Make all focusable elements in the subtree unfocusable and add them to _managedNodes |
|
82 this._makeSubtreeUnfocusable(this._rootElement); |
|
83 |
|
84 // Watch for: |
|
85 // - any additions in the subtree: make them unfocusable too |
|
86 // - any removals from the subtree: remove them from this inert root's managed nodes |
|
87 // - attribute changes: if `tabindex` is added, or removed from an intrinsically focusable |
|
88 // element, make that node a managed node. |
|
89 this._observer = new MutationObserver(this._onMutation.bind(this)); |
|
90 this._observer.observe(this._rootElement, { attributes: true, childList: true, subtree: true }); |
|
91 } |
|
92 |
|
93 /** |
|
94 * Call this whenever this object is about to become obsolete. This unwinds all of the state |
|
95 * stored in this object and updates the state of all of the managed nodes. |
|
96 */ |
|
97 |
|
98 |
|
99 _createClass(InertRoot, [{ |
|
100 key: 'destructor', |
|
101 value: function destructor() { |
|
102 this._observer.disconnect(); |
|
103 |
|
104 if (this._rootElement) { |
|
105 if (this._savedAriaHidden !== null) { |
|
106 this._rootElement.setAttribute('aria-hidden', this._savedAriaHidden); |
|
107 } else { |
|
108 this._rootElement.removeAttribute('aria-hidden'); |
|
109 } |
|
110 } |
|
111 |
|
112 this._managedNodes.forEach(function (inertNode) { |
|
113 this._unmanageNode(inertNode.node); |
|
114 }, this); |
|
115 |
|
116 // Note we cast the nulls to the ANY type here because: |
|
117 // 1) We want the class properties to be declared as non-null, or else we |
|
118 // need even more casts throughout this code. All bets are off if an |
|
119 // instance has been destroyed and a method is called. |
|
120 // 2) We don't want to cast "this", because we want type-aware optimizations |
|
121 // to know which properties we're setting. |
|
122 this._observer = /** @type {?} */null; |
|
123 this._rootElement = /** @type {?} */null; |
|
124 this._managedNodes = /** @type {?} */null; |
|
125 this._inertManager = /** @type {?} */null; |
|
126 } |
|
127 |
|
128 /** |
|
129 * @return {!Set<!InertNode>} A copy of this InertRoot's managed nodes set. |
|
130 */ |
|
131 |
|
132 }, { |
|
133 key: '_makeSubtreeUnfocusable', |
|
134 |
|
135 |
|
136 /** |
|
137 * @param {!Node} startNode |
|
138 */ |
|
139 value: function _makeSubtreeUnfocusable(startNode) { |
|
140 var _this2 = this; |
|
141 |
|
142 composedTreeWalk(startNode, function (node) { |
|
143 return _this2._visitNode(node); |
|
144 }); |
|
145 |
|
146 var activeElement = document.activeElement; |
|
147 |
|
148 if (!document.body.contains(startNode)) { |
|
149 // startNode may be in shadow DOM, so find its nearest shadowRoot to get the activeElement. |
|
150 var node = startNode; |
|
151 /** @type {!ShadowRoot|undefined} */ |
|
152 var root = undefined; |
|
153 while (node) { |
|
154 if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { |
|
155 root = /** @type {!ShadowRoot} */node; |
|
156 break; |
|
157 } |
|
158 node = node.parentNode; |
|
159 } |
|
160 if (root) { |
|
161 activeElement = root.activeElement; |
|
162 } |
|
163 } |
|
164 if (startNode.contains(activeElement)) { |
|
165 activeElement.blur(); |
|
166 // In IE11, if an element is already focused, and then set to tabindex=-1 |
|
167 // calling blur() will not actually move the focus. |
|
168 // To work around this we call focus() on the body instead. |
|
169 if (activeElement === document.activeElement) { |
|
170 document.body.focus(); |
|
171 } |
|
172 } |
|
173 } |
|
174 |
|
175 /** |
|
176 * @param {!Node} node |
|
177 */ |
|
178 |
|
179 }, { |
|
180 key: '_visitNode', |
|
181 value: function _visitNode(node) { |
|
182 if (node.nodeType !== Node.ELEMENT_NODE) { |
|
183 return; |
|
184 } |
|
185 var element = /** @type {!HTMLElement} */node; |
|
186 |
|
187 // If a descendant inert root becomes un-inert, its descendants will still be inert because of |
|
188 // this inert root, so all of its managed nodes need to be adopted by this InertRoot. |
|
189 if (element !== this._rootElement && element.hasAttribute('inert')) { |
|
190 this._adoptInertRoot(element); |
|
191 } |
|
192 |
|
193 if (matches.call(element, _focusableElementsString) || element.hasAttribute('tabindex')) { |
|
194 this._manageNode(element); |
|
195 } |
|
196 } |
|
197 |
|
198 /** |
|
199 * Register the given node with this InertRoot and with InertManager. |
|
200 * @param {!Node} node |
|
201 */ |
|
202 |
|
203 }, { |
|
204 key: '_manageNode', |
|
205 value: function _manageNode(node) { |
|
206 var inertNode = this._inertManager.register(node, this); |
|
207 this._managedNodes.add(inertNode); |
|
208 } |
|
209 |
|
210 /** |
|
211 * Unregister the given node with this InertRoot and with InertManager. |
|
212 * @param {!Node} node |
|
213 */ |
|
214 |
|
215 }, { |
|
216 key: '_unmanageNode', |
|
217 value: function _unmanageNode(node) { |
|
218 var inertNode = this._inertManager.deregister(node, this); |
|
219 if (inertNode) { |
|
220 this._managedNodes['delete'](inertNode); |
|
221 } |
|
222 } |
|
223 |
|
224 /** |
|
225 * Unregister the entire subtree starting at `startNode`. |
|
226 * @param {!Node} startNode |
|
227 */ |
|
228 |
|
229 }, { |
|
230 key: '_unmanageSubtree', |
|
231 value: function _unmanageSubtree(startNode) { |
|
232 var _this3 = this; |
|
233 |
|
234 composedTreeWalk(startNode, function (node) { |
|
235 return _this3._unmanageNode(node); |
|
236 }); |
|
237 } |
|
238 |
|
239 /** |
|
240 * If a descendant node is found with an `inert` attribute, adopt its managed nodes. |
|
241 * @param {!HTMLElement} node |
|
242 */ |
|
243 |
|
244 }, { |
|
245 key: '_adoptInertRoot', |
|
246 value: function _adoptInertRoot(node) { |
|
247 var inertSubroot = this._inertManager.getInertRoot(node); |
|
248 |
|
249 // During initialisation this inert root may not have been registered yet, |
|
250 // so register it now if need be. |
|
251 if (!inertSubroot) { |
|
252 this._inertManager.setInert(node, true); |
|
253 inertSubroot = this._inertManager.getInertRoot(node); |
|
254 } |
|
255 |
|
256 inertSubroot.managedNodes.forEach(function (savedInertNode) { |
|
257 this._manageNode(savedInertNode.node); |
|
258 }, this); |
|
259 } |
|
260 |
|
261 /** |
|
262 * Callback used when mutation observer detects subtree additions, removals, or attribute changes. |
|
263 * @param {!Array<!MutationRecord>} records |
|
264 * @param {!MutationObserver} self |
|
265 */ |
|
266 |
|
267 }, { |
|
268 key: '_onMutation', |
|
269 value: function _onMutation(records, self) { |
|
270 records.forEach(function (record) { |
|
271 var target = /** @type {!HTMLElement} */record.target; |
|
272 if (record.type === 'childList') { |
|
273 // Manage added nodes |
|
274 slice.call(record.addedNodes).forEach(function (node) { |
|
275 this._makeSubtreeUnfocusable(node); |
|
276 }, this); |
|
277 |
|
278 // Un-manage removed nodes |
|
279 slice.call(record.removedNodes).forEach(function (node) { |
|
280 this._unmanageSubtree(node); |
|
281 }, this); |
|
282 } else if (record.type === 'attributes') { |
|
283 if (record.attributeName === 'tabindex') { |
|
284 // Re-initialise inert node if tabindex changes |
|
285 this._manageNode(target); |
|
286 } else if (target !== this._rootElement && record.attributeName === 'inert' && target.hasAttribute('inert')) { |
|
287 // If a new inert root is added, adopt its managed nodes and make sure it knows about the |
|
288 // already managed nodes from this inert subroot. |
|
289 this._adoptInertRoot(target); |
|
290 var inertSubroot = this._inertManager.getInertRoot(target); |
|
291 this._managedNodes.forEach(function (managedNode) { |
|
292 if (target.contains(managedNode.node)) { |
|
293 inertSubroot._manageNode(managedNode.node); |
|
294 } |
|
295 }); |
|
296 } |
|
297 } |
|
298 }, this); |
|
299 } |
|
300 }, { |
|
301 key: 'managedNodes', |
|
302 get: function get() { |
|
303 return new Set(this._managedNodes); |
|
304 } |
|
305 |
|
306 /** @return {boolean} */ |
|
307 |
|
308 }, { |
|
309 key: 'hasSavedAriaHidden', |
|
310 get: function get() { |
|
311 return this._savedAriaHidden !== null; |
|
312 } |
|
313 |
|
314 /** @param {?string} ariaHidden */ |
|
315 |
|
316 }, { |
|
317 key: 'savedAriaHidden', |
|
318 set: function set(ariaHidden) { |
|
319 this._savedAriaHidden = ariaHidden; |
|
320 } |
|
321 |
|
322 /** @return {?string} */ |
|
323 , |
|
324 get: function get() { |
|
325 return this._savedAriaHidden; |
|
326 } |
|
327 }]); |
|
328 |
|
329 return InertRoot; |
|
330 }(); |
|
331 |
|
332 /** |
|
333 * `InertNode` initialises and manages a single inert node. |
|
334 * A node is inert if it is a descendant of one or more inert root elements. |
|
335 * |
|
336 * On construction, `InertNode` saves the existing `tabindex` value for the node, if any, and |
|
337 * either removes the `tabindex` attribute or sets it to `-1`, depending on whether the element |
|
338 * is intrinsically focusable or not. |
|
339 * |
|
340 * `InertNode` maintains a set of `InertRoot`s which are descendants of this `InertNode`. When an |
|
341 * `InertRoot` is destroyed, and calls `InertManager.deregister()`, the `InertManager` notifies the |
|
342 * `InertNode` via `removeInertRoot()`, which in turn destroys the `InertNode` if no `InertRoot`s |
|
343 * remain in the set. On destruction, `InertNode` reinstates the stored `tabindex` if one exists, |
|
344 * or removes the `tabindex` attribute if the element is intrinsically focusable. |
|
345 */ |
|
346 |
|
347 |
|
348 var InertNode = function () { |
|
349 /** |
|
350 * @param {!Node} node A focusable element to be made inert. |
|
351 * @param {!InertRoot} inertRoot The inert root element associated with this inert node. |
|
352 */ |
|
353 function InertNode(node, inertRoot) { |
|
354 _classCallCheck(this, InertNode); |
|
355 |
|
356 /** @type {!Node} */ |
|
357 this._node = node; |
|
358 |
|
359 /** @type {boolean} */ |
|
360 this._overrodeFocusMethod = false; |
|
361 |
|
362 /** |
|
363 * @type {!Set<!InertRoot>} The set of descendant inert roots. |
|
364 * If and only if this set becomes empty, this node is no longer inert. |
|
365 */ |
|
366 this._inertRoots = new Set([inertRoot]); |
|
367 |
|
368 /** @type {?number} */ |
|
369 this._savedTabIndex = null; |
|
370 |
|
371 /** @type {boolean} */ |
|
372 this._destroyed = false; |
|
373 |
|
374 // Save any prior tabindex info and make this node untabbable |
|
375 this.ensureUntabbable(); |
|
376 } |
|
377 |
|
378 /** |
|
379 * Call this whenever this object is about to become obsolete. |
|
380 * This makes the managed node focusable again and deletes all of the previously stored state. |
|
381 */ |
|
382 |
|
383 |
|
384 _createClass(InertNode, [{ |
|
385 key: 'destructor', |
|
386 value: function destructor() { |
|
387 this._throwIfDestroyed(); |
|
388 |
|
389 if (this._node && this._node.nodeType === Node.ELEMENT_NODE) { |
|
390 var element = /** @type {!HTMLElement} */this._node; |
|
391 if (this._savedTabIndex !== null) { |
|
392 element.setAttribute('tabindex', this._savedTabIndex); |
|
393 } else { |
|
394 element.removeAttribute('tabindex'); |
|
395 } |
|
396 |
|
397 // Use `delete` to restore native focus method. |
|
398 if (this._overrodeFocusMethod) { |
|
399 delete element.focus; |
|
400 } |
|
401 } |
|
402 |
|
403 // See note in InertRoot.destructor for why we cast these nulls to ANY. |
|
404 this._node = /** @type {?} */null; |
|
405 this._inertRoots = /** @type {?} */null; |
|
406 this._destroyed = true; |
|
407 } |
|
408 |
|
409 /** |
|
410 * @type {boolean} Whether this object is obsolete because the managed node is no longer inert. |
|
411 * If the object has been destroyed, any attempt to access it will cause an exception. |
|
412 */ |
|
413 |
|
414 }, { |
|
415 key: '_throwIfDestroyed', |
|
416 |
|
417 |
|
418 /** |
|
419 * Throw if user tries to access destroyed InertNode. |
|
420 */ |
|
421 value: function _throwIfDestroyed() { |
|
422 if (this.destroyed) { |
|
423 throw new Error('Trying to access destroyed InertNode'); |
|
424 } |
|
425 } |
|
426 |
|
427 /** @return {boolean} */ |
|
428 |
|
429 }, { |
|
430 key: 'ensureUntabbable', |
|
431 |
|
432 |
|
433 /** Save the existing tabindex value and make the node untabbable and unfocusable */ |
|
434 value: function ensureUntabbable() { |
|
435 if (this.node.nodeType !== Node.ELEMENT_NODE) { |
|
436 return; |
|
437 } |
|
438 var element = /** @type {!HTMLElement} */this.node; |
|
439 if (matches.call(element, _focusableElementsString)) { |
|
440 if ( /** @type {!HTMLElement} */element.tabIndex === -1 && this.hasSavedTabIndex) { |
|
441 return; |
|
442 } |
|
443 |
|
444 if (element.hasAttribute('tabindex')) { |
|
445 this._savedTabIndex = /** @type {!HTMLElement} */element.tabIndex; |
|
446 } |
|
447 element.setAttribute('tabindex', '-1'); |
|
448 if (element.nodeType === Node.ELEMENT_NODE) { |
|
449 element.focus = function () {}; |
|
450 this._overrodeFocusMethod = true; |
|
451 } |
|
452 } else if (element.hasAttribute('tabindex')) { |
|
453 this._savedTabIndex = /** @type {!HTMLElement} */element.tabIndex; |
|
454 element.removeAttribute('tabindex'); |
|
455 } |
|
456 } |
|
457 |
|
458 /** |
|
459 * Add another inert root to this inert node's set of managing inert roots. |
|
460 * @param {!InertRoot} inertRoot |
|
461 */ |
|
462 |
|
463 }, { |
|
464 key: 'addInertRoot', |
|
465 value: function addInertRoot(inertRoot) { |
|
466 this._throwIfDestroyed(); |
|
467 this._inertRoots.add(inertRoot); |
|
468 } |
|
469 |
|
470 /** |
|
471 * Remove the given inert root from this inert node's set of managing inert roots. |
|
472 * If the set of managing inert roots becomes empty, this node is no longer inert, |
|
473 * so the object should be destroyed. |
|
474 * @param {!InertRoot} inertRoot |
|
475 */ |
|
476 |
|
477 }, { |
|
478 key: 'removeInertRoot', |
|
479 value: function removeInertRoot(inertRoot) { |
|
480 this._throwIfDestroyed(); |
|
481 this._inertRoots['delete'](inertRoot); |
|
482 if (this._inertRoots.size === 0) { |
|
483 this.destructor(); |
|
484 } |
|
485 } |
|
486 }, { |
|
487 key: 'destroyed', |
|
488 get: function get() { |
|
489 return (/** @type {!InertNode} */this._destroyed |
|
490 ); |
|
491 } |
|
492 }, { |
|
493 key: 'hasSavedTabIndex', |
|
494 get: function get() { |
|
495 return this._savedTabIndex !== null; |
|
496 } |
|
497 |
|
498 /** @return {!Node} */ |
|
499 |
|
500 }, { |
|
501 key: 'node', |
|
502 get: function get() { |
|
503 this._throwIfDestroyed(); |
|
504 return this._node; |
|
505 } |
|
506 |
|
507 /** @param {?number} tabIndex */ |
|
508 |
|
509 }, { |
|
510 key: 'savedTabIndex', |
|
511 set: function set(tabIndex) { |
|
512 this._throwIfDestroyed(); |
|
513 this._savedTabIndex = tabIndex; |
|
514 } |
|
515 |
|
516 /** @return {?number} */ |
|
517 , |
|
518 get: function get() { |
|
519 this._throwIfDestroyed(); |
|
520 return this._savedTabIndex; |
|
521 } |
|
522 }]); |
|
523 |
|
524 return InertNode; |
|
525 }(); |
|
526 |
|
527 /** |
|
528 * InertManager is a per-document singleton object which manages all inert roots and nodes. |
|
529 * |
|
530 * When an element becomes an inert root by having an `inert` attribute set and/or its `inert` |
|
531 * property set to `true`, the `setInert` method creates an `InertRoot` object for the element. |
|
532 * The `InertRoot` in turn registers itself as managing all of the element's focusable descendant |
|
533 * nodes via the `register()` method. The `InertManager` ensures that a single `InertNode` instance |
|
534 * is created for each such node, via the `_managedNodes` map. |
|
535 */ |
|
536 |
|
537 |
|
538 var InertManager = function () { |
|
539 /** |
|
540 * @param {!Document} document |
|
541 */ |
|
542 function InertManager(document) { |
|
543 _classCallCheck(this, InertManager); |
|
544 |
|
545 if (!document) { |
|
546 throw new Error('Missing required argument; InertManager needs to wrap a document.'); |
|
547 } |
|
548 |
|
549 /** @type {!Document} */ |
|
550 this._document = document; |
|
551 |
|
552 /** |
|
553 * All managed nodes known to this InertManager. In a map to allow looking up by Node. |
|
554 * @type {!Map<!Node, !InertNode>} |
|
555 */ |
|
556 this._managedNodes = new Map(); |
|
557 |
|
558 /** |
|
559 * All inert roots known to this InertManager. In a map to allow looking up by Node. |
|
560 * @type {!Map<!Node, !InertRoot>} |
|
561 */ |
|
562 this._inertRoots = new Map(); |
|
563 |
|
564 /** |
|
565 * Observer for mutations on `document.body`. |
|
566 * @type {!MutationObserver} |
|
567 */ |
|
568 this._observer = new MutationObserver(this._watchForInert.bind(this)); |
|
569 |
|
570 // Add inert style. |
|
571 addInertStyle(document.head || document.body || document.documentElement); |
|
572 |
|
573 // Wait for document to be loaded. |
|
574 if (document.readyState === 'loading') { |
|
575 document.addEventListener('DOMContentLoaded', this._onDocumentLoaded.bind(this)); |
|
576 } else { |
|
577 this._onDocumentLoaded(); |
|
578 } |
|
579 } |
|
580 |
|
581 /** |
|
582 * Set whether the given element should be an inert root or not. |
|
583 * @param {!HTMLElement} root |
|
584 * @param {boolean} inert |
|
585 */ |
|
586 |
|
587 |
|
588 _createClass(InertManager, [{ |
|
589 key: 'setInert', |
|
590 value: function setInert(root, inert) { |
|
591 if (inert) { |
|
592 if (this._inertRoots.has(root)) { |
|
593 // element is already inert |
|
594 return; |
|
595 } |
|
596 |
|
597 var inertRoot = new InertRoot(root, this); |
|
598 root.setAttribute('inert', ''); |
|
599 this._inertRoots.set(root, inertRoot); |
|
600 // If not contained in the document, it must be in a shadowRoot. |
|
601 // Ensure inert styles are added there. |
|
602 if (!this._document.body.contains(root)) { |
|
603 var parent = root.parentNode; |
|
604 while (parent) { |
|
605 if (parent.nodeType === 11) { |
|
606 addInertStyle(parent); |
|
607 } |
|
608 parent = parent.parentNode; |
|
609 } |
|
610 } |
|
611 } else { |
|
612 if (!this._inertRoots.has(root)) { |
|
613 // element is already non-inert |
|
614 return; |
|
615 } |
|
616 |
|
617 var _inertRoot = this._inertRoots.get(root); |
|
618 _inertRoot.destructor(); |
|
619 this._inertRoots['delete'](root); |
|
620 root.removeAttribute('inert'); |
|
621 } |
|
622 } |
|
623 |
|
624 /** |
|
625 * Get the InertRoot object corresponding to the given inert root element, if any. |
|
626 * @param {!Node} element |
|
627 * @return {!InertRoot|undefined} |
|
628 */ |
|
629 |
|
630 }, { |
|
631 key: 'getInertRoot', |
|
632 value: function getInertRoot(element) { |
|
633 return this._inertRoots.get(element); |
|
634 } |
|
635 |
|
636 /** |
|
637 * Register the given InertRoot as managing the given node. |
|
638 * In the case where the node has a previously existing inert root, this inert root will |
|
639 * be added to its set of inert roots. |
|
640 * @param {!Node} node |
|
641 * @param {!InertRoot} inertRoot |
|
642 * @return {!InertNode} inertNode |
|
643 */ |
|
644 |
|
645 }, { |
|
646 key: 'register', |
|
647 value: function register(node, inertRoot) { |
|
648 var inertNode = this._managedNodes.get(node); |
|
649 if (inertNode !== undefined) { |
|
650 // node was already in an inert subtree |
|
651 inertNode.addInertRoot(inertRoot); |
|
652 } else { |
|
653 inertNode = new InertNode(node, inertRoot); |
|
654 } |
|
655 |
|
656 this._managedNodes.set(node, inertNode); |
|
657 |
|
658 return inertNode; |
|
659 } |
|
660 |
|
661 /** |
|
662 * De-register the given InertRoot as managing the given inert node. |
|
663 * Removes the inert root from the InertNode's set of managing inert roots, and remove the inert |
|
664 * node from the InertManager's set of managed nodes if it is destroyed. |
|
665 * If the node is not currently managed, this is essentially a no-op. |
|
666 * @param {!Node} node |
|
667 * @param {!InertRoot} inertRoot |
|
668 * @return {?InertNode} The potentially destroyed InertNode associated with this node, if any. |
|
669 */ |
|
670 |
|
671 }, { |
|
672 key: 'deregister', |
|
673 value: function deregister(node, inertRoot) { |
|
674 var inertNode = this._managedNodes.get(node); |
|
675 if (!inertNode) { |
|
676 return null; |
|
677 } |
|
678 |
|
679 inertNode.removeInertRoot(inertRoot); |
|
680 if (inertNode.destroyed) { |
|
681 this._managedNodes['delete'](node); |
|
682 } |
|
683 |
|
684 return inertNode; |
|
685 } |
|
686 |
|
687 /** |
|
688 * Callback used when document has finished loading. |
|
689 */ |
|
690 |
|
691 }, { |
|
692 key: '_onDocumentLoaded', |
|
693 value: function _onDocumentLoaded() { |
|
694 // Find all inert roots in document and make them actually inert. |
|
695 var inertElements = slice.call(this._document.querySelectorAll('[inert]')); |
|
696 inertElements.forEach(function (inertElement) { |
|
697 this.setInert(inertElement, true); |
|
698 }, this); |
|
699 |
|
700 // Comment this out to use programmatic API only. |
|
701 this._observer.observe(this._document.body || this._document.documentElement, { attributes: true, subtree: true, childList: true }); |
|
702 } |
|
703 |
|
704 /** |
|
705 * Callback used when mutation observer detects attribute changes. |
|
706 * @param {!Array<!MutationRecord>} records |
|
707 * @param {!MutationObserver} self |
|
708 */ |
|
709 |
|
710 }, { |
|
711 key: '_watchForInert', |
|
712 value: function _watchForInert(records, self) { |
|
713 var _this = this; |
|
714 records.forEach(function (record) { |
|
715 switch (record.type) { |
|
716 case 'childList': |
|
717 slice.call(record.addedNodes).forEach(function (node) { |
|
718 if (node.nodeType !== Node.ELEMENT_NODE) { |
|
719 return; |
|
720 } |
|
721 var inertElements = slice.call(node.querySelectorAll('[inert]')); |
|
722 if (matches.call(node, '[inert]')) { |
|
723 inertElements.unshift(node); |
|
724 } |
|
725 inertElements.forEach(function (inertElement) { |
|
726 this.setInert(inertElement, true); |
|
727 }, _this); |
|
728 }, _this); |
|
729 break; |
|
730 case 'attributes': |
|
731 if (record.attributeName !== 'inert') { |
|
732 return; |
|
733 } |
|
734 var target = /** @type {!HTMLElement} */record.target; |
|
735 var inert = target.hasAttribute('inert'); |
|
736 _this.setInert(target, inert); |
|
737 break; |
|
738 } |
|
739 }, this); |
|
740 } |
|
741 }]); |
|
742 |
|
743 return InertManager; |
|
744 }(); |
|
745 |
|
746 /** |
|
747 * Recursively walk the composed tree from |node|. |
|
748 * @param {!Node} node |
|
749 * @param {(function (!HTMLElement))=} callback Callback to be called for each element traversed, |
|
750 * before descending into child nodes. |
|
751 * @param {?ShadowRoot=} shadowRootAncestor The nearest ShadowRoot ancestor, if any. |
|
752 */ |
|
753 |
|
754 |
|
755 function composedTreeWalk(node, callback, shadowRootAncestor) { |
|
756 if (node.nodeType == Node.ELEMENT_NODE) { |
|
757 var element = /** @type {!HTMLElement} */node; |
|
758 if (callback) { |
|
759 callback(element); |
|
760 } |
|
761 |
|
762 // Descend into node: |
|
763 // If it has a ShadowRoot, ignore all child elements - these will be picked |
|
764 // up by the <content> or <shadow> elements. Descend straight into the |
|
765 // ShadowRoot. |
|
766 var shadowRoot = /** @type {!HTMLElement} */element.shadowRoot; |
|
767 if (shadowRoot) { |
|
768 composedTreeWalk(shadowRoot, callback, shadowRoot); |
|
769 return; |
|
770 } |
|
771 |
|
772 // If it is a <content> element, descend into distributed elements - these |
|
773 // are elements from outside the shadow root which are rendered inside the |
|
774 // shadow DOM. |
|
775 if (element.localName == 'content') { |
|
776 var content = /** @type {!HTMLContentElement} */element; |
|
777 // Verifies if ShadowDom v0 is supported. |
|
778 var distributedNodes = content.getDistributedNodes ? content.getDistributedNodes() : []; |
|
779 for (var i = 0; i < distributedNodes.length; i++) { |
|
780 composedTreeWalk(distributedNodes[i], callback, shadowRootAncestor); |
|
781 } |
|
782 return; |
|
783 } |
|
784 |
|
785 // If it is a <slot> element, descend into assigned nodes - these |
|
786 // are elements from outside the shadow root which are rendered inside the |
|
787 // shadow DOM. |
|
788 if (element.localName == 'slot') { |
|
789 var slot = /** @type {!HTMLSlotElement} */element; |
|
790 // Verify if ShadowDom v1 is supported. |
|
791 var _distributedNodes = slot.assignedNodes ? slot.assignedNodes({ flatten: true }) : []; |
|
792 for (var _i = 0; _i < _distributedNodes.length; _i++) { |
|
793 composedTreeWalk(_distributedNodes[_i], callback, shadowRootAncestor); |
|
794 } |
|
795 return; |
|
796 } |
|
797 } |
|
798 |
|
799 // If it is neither the parent of a ShadowRoot, a <content> element, a <slot> |
|
800 // element, nor a <shadow> element recurse normally. |
|
801 var child = node.firstChild; |
|
802 while (child != null) { |
|
803 composedTreeWalk(child, callback, shadowRootAncestor); |
|
804 child = child.nextSibling; |
|
805 } |
|
806 } |
|
807 |
|
808 /** |
|
809 * Adds a style element to the node containing the inert specific styles |
|
810 * @param {!Node} node |
|
811 */ |
|
812 function addInertStyle(node) { |
|
813 if (node.querySelector('style#inert-style, link#inert-style')) { |
|
814 return; |
|
815 } |
|
816 var style = document.createElement('style'); |
|
817 style.setAttribute('id', 'inert-style'); |
|
818 style.textContent = '\n' + '[inert] {\n' + ' pointer-events: none;\n' + ' cursor: default;\n' + '}\n' + '\n' + '[inert], [inert] * {\n' + ' -webkit-user-select: none;\n' + ' -moz-user-select: none;\n' + ' -ms-user-select: none;\n' + ' user-select: none;\n' + '}\n'; |
|
819 node.appendChild(style); |
|
820 } |
|
821 |
|
822 if (!HTMLElement.prototype.hasOwnProperty('inert')) { |
|
823 /** @type {!InertManager} */ |
|
824 var inertManager = new InertManager(document); |
|
825 |
|
826 Object.defineProperty(HTMLElement.prototype, 'inert', { |
|
827 enumerable: true, |
|
828 /** @this {!HTMLElement} */ |
|
829 get: function get() { |
|
830 return this.hasAttribute('inert'); |
|
831 }, |
|
832 /** @this {!HTMLElement} */ |
|
833 set: function set(inert) { |
|
834 inertManager.setInert(this, inert); |
|
835 } |
|
836 }); |
|
837 } |
|
838 })(); |
|
839 |
|
840 }))); |