Standard 1up Carousel with thumbnail and next/previous links.
28 |
This carousel starts with HTML containing slides with focusable (linked) content inside them and linked thumbnail navigation, which are regular anchor links to each slide's corresponding ID attribute. (We suggest putting those first in the source order, if you include them.) It also has next/prev links that are automatically added through the addition of a data-carousel-nextprev attribute.
Example w/ varying number of slides showing depending on viewport size
200 |
This example plays nicely with CSS breakpoints to show a different number of slides depending on the viewport size. To use breakpoints in this way, for back compat, be sure to include Snap Points that correspond to the item widths. See CSS for this example.
If you set slides to a width that doesn't divide evenly in the visible viewport, you'll have slides that partially reveal, which can be a nice affordance to suggest to the user that there's more content to see.
This example is similar to prior examples that show multiple slides, but it has fixed-width slides, rather than slides that fill a percent of the viewport. It also uses dynamic pagination for thumbnails and arrows through the "data-carousel-paginated" attribute. Pagination will cause the thumbnails and arrows to treat the visible slides as one unit, advancing as a whole, which tends to work better for multiple slides. Regardless of whether widths are fixed or fluid, if the number of slides showing at any time varies such as in this example, the number of dots may change across breakpoints. Dynamic thumbnails highlight one "viewport" at a time.
This example is similar to prior examples that show multiple slides, but it uses dynamic pagination for thumbnails and arrows through the "data-carousel-paginated" attribute. Pagination will cause the thumbnails and arrows to treat the visible slides as one unit, advancing as a whole, which tends to work better for multiple slides. The number of dots in the nav may change across breakpoints to match the number of "pages" that are visible.
By setting the data-carousel-autoplay attribute on the fg-carousel element to a natural number value carousel will automatically rotate through the images. The value represents a the millisecond delay between item transitions. In the example below we have data-carousel-autoplay="4000"
452 |
You can also set the attribute on carousel_item elements to get individual timing.
A carousel carousel with data-carousel-loop will append items to either end as needed so the scroll is infinite. This is recommended for 1-slide-at-a-time carousels.
500 |
501 |
502 |
503 |
504 |
505 |
506 |
507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 |
516 |
517 |
518 |
519 |
520 |
521 |
522 |
523 |
524 |
525 |
526 |
527 |
528 |
529 |
530 |
531 | ## About
532 |
533 | This carousel component is built to be easy to use, dependency-free (aside from feature polyfills), and accessible.
534 |
535 |
536 |
537 | ## Documentation
538 |
539 | To make a carousel, create a `fg-carousel` element to contain your content. This element will be recognized by this component's javascript, and allow it to be enhanced with necessary behaviors and accessibility information.
540 |
541 | Inside the carousel element, place one or more items that will become carousel items that snap scroll and fill 100% of the width.
542 |
543 |
544 |
545 |
546 | ## Including Scripts & Styles
547 |
548 | The carousel has some dependencies, one for the Javascript and one for the CSS, which you can find in the `src` directory:
549 |
550 | ```html
551 |
552 |
553 |
554 |
555 |
556 |
557 | ```
558 |
559 | Note: to support IE11, we have used Babel to create [a module-free version of the carousel](demo/es5/fg-carousel.js) in the `demo` directory, which is listed above using the module/nomodule pattern to only delivery to non-module browsers.
560 |
561 |
562 | ## Methods and Events
563 |
564 | The carousel has several methods you can call on it, such as goto, next, prev. We're still refining this API so they'll be documented soon.
565 |
566 | The carousel has several events.
567 | - Also tbd documentation
568 |
569 | ## Polyfills
570 |
571 | To use the carousel in modern browsers, two polyfills are likely necessary (please check browser support to see how these align with your needs).
572 |
573 | - Custom Elements: The `fg-carousel` element uses the standard HTML custom elements feature, which are well supported but need a polyfill in IE11 and older. This project references WebReflection's [Document Register Element](https://github.com/WebReflection/document-register-element) polyfill which can be found at [demo/lib/document-register-element.js](demo/lib/document-register-element.js). It should be loaded prior to the accessible carousel script. In our demo page we use the following pattern to load it, but you could package it with
574 | - Intersection Observer: The `fg-carousel` element uses the standard intersection observer API to detect visibility of elements in the scroll area. For support, this may need a polyfill. We've [included one in the demos](demo/lib/intersection-observer.js) for convenience, via ``
575 | - Inert: The standard `inert` attribute (support currently includes Chrome and Edge) is used for disabling the rest of the slides that are not active, which helps ensure a clean "tabs" experience when the component is used with assistive tech. Browser support for `inert` is still improving so [WICG's Inert polyfill](https://github.com/WICG/inert) is listed as a dependency of this project and can be found in the [demo/inert.js](demo/inert.js) file. You can load it in a deferred or async manner as it is not used until the dialog is opened. Example: ``
--------------------------------------------------------------------------------
/demo/lib/inert.js:
--------------------------------------------------------------------------------
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 {!Element} rootElement The Element 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 {!Element} */
64 | this._rootElement = rootElement;
65 |
66 | /**
67 | * @type {!Set}
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} 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 {!Element} */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 {!Element} 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} 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 {!Element} */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} 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 {!Element} */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 {!Element} */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}
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}
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 {!Element} 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} 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 {!Element} */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 (!Element))=} 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 {!Element} */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 or 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 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 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 element, a
800 | // element, nor a 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 (!Element.prototype.hasOwnProperty('inert')) {
823 | /** @type {!InertManager} */
824 | var inertManager = new InertManager(document);
825 |
826 | Object.defineProperty(Element.prototype, 'inert', {
827 | enumerable: true,
828 | /** @this {!Element} */
829 | get: function get() {
830 | return this.hasAttribute('inert');
831 | },
832 | /** @this {!Element} */
833 | set: function set(inert) {
834 | inertManager.setInert(this, inert);
835 | }
836 | });
837 | }
838 | })();
839 |
840 | })));
841 |
--------------------------------------------------------------------------------
/demo/lib/intersection-observer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2016 Google Inc. All Rights Reserved.
3 | *
4 | * Licensed under the W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE.
5 | *
6 | * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
7 | *
8 | */
9 | (function() {
10 | 'use strict';
11 |
12 | // Exit early if we're not running in a browser.
13 | if (typeof window !== 'object') {
14 | return;
15 | }
16 |
17 | // Exit early if all IntersectionObserver and IntersectionObserverEntry
18 | // features are natively supported.
19 | if ('IntersectionObserver' in window &&
20 | 'IntersectionObserverEntry' in window &&
21 | 'intersectionRatio' in window.IntersectionObserverEntry.prototype) {
22 |
23 | // Minimal polyfill for Edge 15's lack of `isIntersecting`
24 | // See: https://github.com/w3c/IntersectionObserver/issues/211
25 | if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
26 | Object.defineProperty(window.IntersectionObserverEntry.prototype,
27 | 'isIntersecting', {
28 | get: function () {
29 | return this.intersectionRatio > 0;
30 | }
31 | });
32 | }
33 | return;
34 | }
35 |
36 | /**
37 | * Returns the embedding frame element, if any.
38 | * @param {!Document} doc
39 | * @return {!Element}
40 | */
41 | function getFrameElement(doc) {
42 | try {
43 | return doc.defaultView && doc.defaultView.frameElement || null;
44 | } catch (e) {
45 | // Ignore the error.
46 | return null;
47 | }
48 | }
49 |
50 | /**
51 | * A local reference to the root document.
52 | */
53 | var document = (function(startDoc) {
54 | var doc = startDoc;
55 | var frame = getFrameElement(doc);
56 | while (frame) {
57 | doc = frame.ownerDocument;
58 | frame = getFrameElement(doc);
59 | }
60 | return doc;
61 | })(window.document);
62 |
63 | /**
64 | * An IntersectionObserver registry. This registry exists to hold a strong
65 | * reference to IntersectionObserver instances currently observing a target
66 | * element. Without this registry, instances without another reference may be
67 | * garbage collected.
68 | */
69 | var registry = [];
70 |
71 | /**
72 | * The signal updater for cross-origin intersection. When not null, it means
73 | * that the polyfill is configured to work in a cross-origin mode.
74 | * @type {function(DOMRect|ClientRect, DOMRect|ClientRect)}
75 | */
76 | var crossOriginUpdater = null;
77 |
78 | /**
79 | * The current cross-origin intersection. Only used in the cross-origin mode.
80 | * @type {DOMRect|ClientRect}
81 | */
82 | var crossOriginRect = null;
83 |
84 |
85 | /**
86 | * Creates the global IntersectionObserverEntry constructor.
87 | * https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
88 | * @param {Object} entry A dictionary of instance properties.
89 | * @constructor
90 | */
91 | function IntersectionObserverEntry(entry) {
92 | this.time = entry.time;
93 | this.target = entry.target;
94 | this.rootBounds = ensureDOMRect(entry.rootBounds);
95 | this.boundingClientRect = ensureDOMRect(entry.boundingClientRect);
96 | this.intersectionRect = ensureDOMRect(entry.intersectionRect || getEmptyRect());
97 | this.isIntersecting = !!entry.intersectionRect;
98 |
99 | // Calculates the intersection ratio.
100 | var targetRect = this.boundingClientRect;
101 | var targetArea = targetRect.width * targetRect.height;
102 | var intersectionRect = this.intersectionRect;
103 | var intersectionArea = intersectionRect.width * intersectionRect.height;
104 |
105 | // Sets intersection ratio.
106 | if (targetArea) {
107 | // Round the intersection ratio to avoid floating point math issues:
108 | // https://github.com/w3c/IntersectionObserver/issues/324
109 | this.intersectionRatio = Number((intersectionArea / targetArea).toFixed(4));
110 | } else {
111 | // If area is zero and is intersecting, sets to 1, otherwise to 0
112 | this.intersectionRatio = this.isIntersecting ? 1 : 0;
113 | }
114 | }
115 |
116 |
117 | /**
118 | * Creates the global IntersectionObserver constructor.
119 | * https://w3c.github.io/IntersectionObserver/#intersection-observer-interface
120 | * @param {Function} callback The function to be invoked after intersection
121 | * changes have queued. The function is not invoked if the queue has
122 | * been emptied by calling the `takeRecords` method.
123 | * @param {Object=} opt_options Optional configuration options.
124 | * @constructor
125 | */
126 | function IntersectionObserver(callback, opt_options) {
127 |
128 | var options = opt_options || {};
129 |
130 | if (typeof callback != 'function') {
131 | throw new Error('callback must be a function');
132 | }
133 |
134 | if (options.root && options.root.nodeType != 1) {
135 | throw new Error('root must be an Element');
136 | }
137 |
138 | // Binds and throttles `this._checkForIntersections`.
139 | this._checkForIntersections = throttle(
140 | this._checkForIntersections.bind(this), this.THROTTLE_TIMEOUT);
141 |
142 | // Private properties.
143 | this._callback = callback;
144 | this._observationTargets = [];
145 | this._queuedEntries = [];
146 | this._rootMarginValues = this._parseRootMargin(options.rootMargin);
147 |
148 | // Public properties.
149 | this.thresholds = this._initThresholds(options.threshold);
150 | this.root = options.root || null;
151 | this.rootMargin = this._rootMarginValues.map(function(margin) {
152 | return margin.value + margin.unit;
153 | }).join(' ');
154 |
155 | /** @private @const {!Array} */
156 | this._monitoringDocuments = [];
157 | /** @private @const {!Array} */
158 | this._monitoringUnsubscribes = [];
159 | }
160 |
161 |
162 | /**
163 | * The minimum interval within which the document will be checked for
164 | * intersection changes.
165 | */
166 | IntersectionObserver.prototype.THROTTLE_TIMEOUT = 100;
167 |
168 |
169 | /**
170 | * The frequency in which the polyfill polls for intersection changes.
171 | * this can be updated on a per instance basis and must be set prior to
172 | * calling `observe` on the first target.
173 | */
174 | IntersectionObserver.prototype.POLL_INTERVAL = null;
175 |
176 | /**
177 | * Use a mutation observer on the root element
178 | * to detect intersection changes.
179 | */
180 | IntersectionObserver.prototype.USE_MUTATION_OBSERVER = true;
181 |
182 |
183 | /**
184 | * Sets up the polyfill in the cross-origin mode. The result is the
185 | * updater function that accepts two arguments: `boundingClientRect` and
186 | * `intersectionRect` - just as these fields would be available to the
187 | * parent via `IntersectionObserverEntry`. This function should be called
188 | * each time the iframe receives intersection information from the parent
189 | * window, e.g. via messaging.
190 | * @return {function(DOMRect|ClientRect, DOMRect|ClientRect)}
191 | */
192 | IntersectionObserver._setupCrossOriginUpdater = function() {
193 | if (!crossOriginUpdater) {
194 | /**
195 | * @param {DOMRect|ClientRect} boundingClientRect
196 | * @param {DOMRect|ClientRect} intersectionRect
197 | */
198 | crossOriginUpdater = function(boundingClientRect, intersectionRect) {
199 | if (!boundingClientRect || !intersectionRect) {
200 | crossOriginRect = getEmptyRect();
201 | } else {
202 | crossOriginRect = convertFromParentRect(boundingClientRect, intersectionRect);
203 | }
204 | registry.forEach(function(observer) {
205 | observer._checkForIntersections();
206 | });
207 | };
208 | }
209 | return crossOriginUpdater;
210 | };
211 |
212 |
213 | /**
214 | * Resets the cross-origin mode.
215 | */
216 | IntersectionObserver._resetCrossOriginUpdater = function() {
217 | crossOriginUpdater = null;
218 | crossOriginRect = null;
219 | };
220 |
221 |
222 | /**
223 | * Starts observing a target element for intersection changes based on
224 | * the thresholds values.
225 | * @param {Element} target The DOM element to observe.
226 | */
227 | IntersectionObserver.prototype.observe = function(target) {
228 | var isTargetAlreadyObserved = this._observationTargets.some(function(item) {
229 | return item.element == target;
230 | });
231 |
232 | if (isTargetAlreadyObserved) {
233 | return;
234 | }
235 |
236 | if (!(target && target.nodeType == 1)) {
237 | throw new Error('target must be an Element');
238 | }
239 |
240 | this._registerInstance();
241 | this._observationTargets.push({element: target, entry: null});
242 | this._monitorIntersections(target.ownerDocument);
243 | this._checkForIntersections();
244 | };
245 |
246 |
247 | /**
248 | * Stops observing a target element for intersection changes.
249 | * @param {Element} target The DOM element to observe.
250 | */
251 | IntersectionObserver.prototype.unobserve = function(target) {
252 | this._observationTargets =
253 | this._observationTargets.filter(function(item) {
254 | return item.element != target;
255 | });
256 | this._unmonitorIntersections(target.ownerDocument);
257 | if (this._observationTargets.length == 0) {
258 | this._unregisterInstance();
259 | }
260 | };
261 |
262 |
263 | /**
264 | * Stops observing all target elements for intersection changes.
265 | */
266 | IntersectionObserver.prototype.disconnect = function() {
267 | this._observationTargets = [];
268 | this._unmonitorAllIntersections();
269 | this._unregisterInstance();
270 | };
271 |
272 |
273 | /**
274 | * Returns any queue entries that have not yet been reported to the
275 | * callback and clears the queue. This can be used in conjunction with the
276 | * callback to obtain the absolute most up-to-date intersection information.
277 | * @return {Array} The currently queued entries.
278 | */
279 | IntersectionObserver.prototype.takeRecords = function() {
280 | var records = this._queuedEntries.slice();
281 | this._queuedEntries = [];
282 | return records;
283 | };
284 |
285 |
286 | /**
287 | * Accepts the threshold value from the user configuration object and
288 | * returns a sorted array of unique threshold values. If a value is not
289 | * between 0 and 1 and error is thrown.
290 | * @private
291 | * @param {Array|number=} opt_threshold An optional threshold value or
292 | * a list of threshold values, defaulting to [0].
293 | * @return {Array} A sorted list of unique and valid threshold values.
294 | */
295 | IntersectionObserver.prototype._initThresholds = function(opt_threshold) {
296 | var threshold = opt_threshold || [0];
297 | if (!Array.isArray(threshold)) threshold = [threshold];
298 |
299 | return threshold.sort().filter(function(t, i, a) {
300 | if (typeof t != 'number' || isNaN(t) || t < 0 || t > 1) {
301 | throw new Error('threshold must be a number between 0 and 1 inclusively');
302 | }
303 | return t !== a[i - 1];
304 | });
305 | };
306 |
307 |
308 | /**
309 | * Accepts the rootMargin value from the user configuration object
310 | * and returns an array of the four margin values as an object containing
311 | * the value and unit properties. If any of the values are not properly
312 | * formatted or use a unit other than px or %, and error is thrown.
313 | * @private
314 | * @param {string=} opt_rootMargin An optional rootMargin value,
315 | * defaulting to '0px'.
316 | * @return {Array