├── .gitignore ├── demo ├── tri-down.png ├── tri-right.png ├── imgs │ ├── a-k-kNG1xaJklfA-unsplash.jpg │ ├── marcus-p-oUBjd22gF6w-unsplash.jpg │ ├── andrea-cau-nV7GJmSq3zc-unsplash.jpg │ ├── chuttersnap-ZRFzHWwGm3g-unsplash.jpg │ ├── joey-banks-YApiWyp0lqo-unsplash.jpg │ ├── lance-asper-N9Pf2J656aQ-unsplash.jpg │ ├── thumb-a-k-kNG1xaJklfA-unsplash.jpg │ ├── john-vicente-CMzmQNU-DGE-unsplash.jpg │ ├── joshua-koblin-eqW1MPinEV4-unsplash.jpg │ ├── benjamin-child-7Cdw956mZ4w-unsplash.jpg │ ├── henning-witzel-ukvgqriuOgo-unsplash.jpg │ ├── jonathan-riley-VW8MUbHyxCU-unsplash.jpg │ ├── jonathan-roger-LY1eyQMFeyo-unsplash.jpg │ ├── lance-anderson-PcCQgQ6KGkI-unsplash.jpg │ ├── matt-antonioli-3akA0XDg1_g-unsplash.jpg │ ├── thmb-andrea-cau-nV7GJmSq3zc-unsplash.jpg │ ├── thumb-chuttersnap-ZRFzHWwGm3g-unsplash.jpg │ ├── anthony-intraversato-xr43RescWSA-unsplash.jpg │ ├── thmb-henning-witzel-ukvgqriuOgo-unsplash.jpg │ ├── thmb-jonathan-riley-VW8MUbHyxCU-unsplash.jpg │ ├── thmb-jonathan-roger-LY1eyQMFeyo-unsplash.jpg │ ├── thumb-lance-anderson-PcCQgQ6KGkI-unsplash.jpg │ └── thumb-anthony-intraversato-xr43RescWSA-unsplash.jpg ├── lib │ ├── document-register-element.js │ ├── inert.js │ └── intersection-observer.js ├── es5 │ └── fg-carousel.js └── index.md ├── _config.yml ├── README.md ├── gulpfile.js ├── package.json ├── assets └── css │ └── style.scss ├── LICENSE ├── test ├── index.html ├── qunit.css └── tests.js └── src ├── fg-carousel.css └── fg-carousel.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /demo/tri-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/tri-down.png -------------------------------------------------------------------------------- /demo/tri-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/tri-right.png -------------------------------------------------------------------------------- /demo/imgs/a-k-kNG1xaJklfA-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/a-k-kNG1xaJklfA-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/marcus-p-oUBjd22gF6w-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/marcus-p-oUBjd22gF6w-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/andrea-cau-nV7GJmSq3zc-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/andrea-cau-nV7GJmSq3zc-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/chuttersnap-ZRFzHWwGm3g-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/chuttersnap-ZRFzHWwGm3g-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/joey-banks-YApiWyp0lqo-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/joey-banks-YApiWyp0lqo-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/lance-asper-N9Pf2J656aQ-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/lance-asper-N9Pf2J656aQ-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/thumb-a-k-kNG1xaJklfA-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/thumb-a-k-kNG1xaJklfA-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/john-vicente-CMzmQNU-DGE-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/john-vicente-CMzmQNU-DGE-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/joshua-koblin-eqW1MPinEV4-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/joshua-koblin-eqW1MPinEV4-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/benjamin-child-7Cdw956mZ4w-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/benjamin-child-7Cdw956mZ4w-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/henning-witzel-ukvgqriuOgo-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/henning-witzel-ukvgqriuOgo-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/jonathan-riley-VW8MUbHyxCU-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/jonathan-riley-VW8MUbHyxCU-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/jonathan-roger-LY1eyQMFeyo-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/jonathan-roger-LY1eyQMFeyo-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/lance-anderson-PcCQgQ6KGkI-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/lance-anderson-PcCQgQ6KGkI-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/matt-antonioli-3akA0XDg1_g-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/matt-antonioli-3akA0XDg1_g-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/thmb-andrea-cau-nV7GJmSq3zc-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/thmb-andrea-cau-nV7GJmSq3zc-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/thumb-chuttersnap-ZRFzHWwGm3g-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/thumb-chuttersnap-ZRFzHWwGm3g-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/anthony-intraversato-xr43RescWSA-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/anthony-intraversato-xr43RescWSA-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/thmb-henning-witzel-ukvgqriuOgo-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/thmb-henning-witzel-ukvgqriuOgo-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/thmb-jonathan-riley-VW8MUbHyxCU-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/thmb-jonathan-riley-VW8MUbHyxCU-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/thmb-jonathan-roger-LY1eyQMFeyo-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/thmb-jonathan-roger-LY1eyQMFeyo-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/thumb-lance-anderson-PcCQgQ6KGkI-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/thumb-lance-anderson-PcCQgQ6KGkI-unsplash.jpg -------------------------------------------------------------------------------- /demo/imgs/thumb-anthony-intraversato-xr43RescWSA-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filamentgroup/fg-carousel/HEAD/demo/imgs/thumb-anthony-intraversato-xr43RescWSA-unsplash.jpg -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | title: ' carousel' 2 | description: An accessible carousel web component 3 | theme: jekyll-theme-cayman 4 | show_downloads: true 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :warning: This project is archived and the repository is no longer maintained. 2 | 3 | # fg-carousel 4 | 5 | An accessible carousel web component 6 | - @scottjehl @filamentgroup 7 | - MIT License 8 | 9 | - [demo & docs](https://filamentgroup.github.io/fg-carousel/demo/) 10 | - [tests](https://filamentgroup.github.io/fg-carousel/tests/) 11 | 12 | 13 | - NPM https://www.npmjs.com/package/fg-carousel 14 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const { parallel, series, watch, src, dest } = require('gulp'); 2 | const babel = require('gulp-babel'); 3 | 4 | function copy() { 5 | return src([ 6 | 'node_modules/wicg-inert/dist/inert.js', 7 | 'node_modules/document-register-element/build/document-register-element.js', 8 | 'node_modules/intersection-observer/intersection-observer.js' 9 | ]) 10 | .pipe(dest('demo/lib/')); 11 | } 12 | 13 | function es5() { 14 | return src('src/fg-carousel.js') 15 | .pipe(babel({ 16 | presets: ['@babel/env'] 17 | })) 18 | .pipe(dest('demo/es5/')); 19 | } 20 | 21 | // var qunit = require('gulp-qunit'); 22 | 23 | // gulp.task('test', function() { 24 | // return gulp.src('./test/index.html') 25 | // .pipe(qunit()); 26 | // }); 27 | 28 | 29 | 30 | 31 | 32 | exports.default = series(copy, es5); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.2", 3 | "name": "fg-carousel", 4 | "homepage": "https://github.com/filamentgroup/fg-carousel/", 5 | "author": { 6 | "name": "Filament Group", 7 | "email": "thegroup@filamentgroup.com" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/filamentgroup/fg-carousel.git" 12 | }, 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/filamentgroup/fg-carousel/issues/" 16 | }, 17 | "main": "src/fg-carousel.js", 18 | "dependencies": { 19 | "document-register-element": "^1.14.5", 20 | "intersection-observer": "^0.11.0", 21 | "wicg-inert": "^3.0.3" 22 | }, 23 | "preview_url": "https://filamentgroup.github.io/fg-carousel", 24 | "devDependencies": { 25 | "gulp": "^4.0.2", 26 | "@babel/core": "^7.11.1", 27 | "@babel/preset-env": "^7.11.0", 28 | "gulp-babel": "^8.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "{{ site.theme }}"; 5 | 6 | .page-header { 7 | background: linear-gradient(#1f7300,#4f9307); 8 | padding: 2rem 4rem; 9 | } 10 | .project-logo { 11 | background: url(https://www.filamentgroup.com/images/svg/fg-logo.svg) 50% 50% no-repeat; 12 | background-size: 90% auto; 13 | display: block; 14 | margin: .2em auto 0; 15 | text-indent: -9999px; 16 | overflow: hidden; 17 | max-width: 170px; 18 | height: 30px 19 | 20 | } 21 | .main-content h1, .main-content h2, .main-content h3, .main-content h4, .main-content h5, .main-content h6 { 22 | color: #2c9915; 23 | } 24 | .project-tagline { 25 | margin-top: .2rem; 26 | opacity: .9; 27 | } 28 | 29 | 30 | .btn { 31 | color: rgba(255,255,255,0.9); 32 | } 33 | 34 | 35 | .btn:hover { 36 | color: rgba(255,255,255,1); 37 | text-decoration: none; 38 | background-color: rgba(255,255,255,0.1); 39 | border-color: rgba(255,255,255,0.6); 40 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Filament Group 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | carousel Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 | 25 | 49 | 56 | 57 |
58 | 59 | 60 | -------------------------------------------------------------------------------- /src/fg-carousel.css: -------------------------------------------------------------------------------- 1 | /* carousel css snap points carousel */ 2 | .carousel { 3 | display: block; 4 | display: flex; 5 | flex-flow: column; 6 | } 7 | .carousel * { 8 | box-sizing: border-box; 9 | } 10 | .carousel_nav { 11 | order: 2; 12 | } 13 | 14 | .carousel, 15 | .carousel_nextprev_contain { 16 | position: relative; 17 | } 18 | 19 | .carousel_item:focus { 20 | /* carousel div receives a tabindex to allow focus for keyboard arrow control */ 21 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); 22 | } 23 | 24 | @supports (scroll-snap-type: mandatory) { 25 | .carousel_pane { 26 | /* IE and edge */ 27 | -ms-overflow-style: none; 28 | /* Firefox */ 29 | scrollbar-width: none; 30 | } 31 | } 32 | .carousel_pane { 33 | scroll-behavior: smooth; 34 | } 35 | 36 | .carousel_pane::-webkit-scrollbar { 37 | display: none; 38 | } 39 | 40 | .carousel_pane { 41 | overflow: auto; 42 | scroll-behavior: smooth; 43 | width: 100%; 44 | /* keep old API for iOS older than 13, then use new API */ 45 | -webkit-overflow-scrolling: touch; 46 | /* snap to points */ 47 | scroll-snap-type: mandatory; 48 | scroll-snap-type: x mandatory; 49 | /* x interval for snapping (100% of container width) */ 50 | scroll-snap-points-x: repeat(100%); 51 | scroll-snap-stop: always; 52 | position: relative; 53 | z-index: 0; 54 | } 55 | 56 | .carousel-sliding .carousel_pane { 57 | scroll-snap-type: none; 58 | } 59 | 60 | .carousel_items { 61 | display: flex; 62 | flex-flow: row nowrap; 63 | } 64 | 65 | .carousel_items > *, 66 | .carousel_item { 67 | position: relative; 68 | white-space: normal; 69 | scroll-snap-align: start; 70 | box-sizing: border-box; 71 | padding-right: 1px; 72 | padding-left: 1px; 73 | flex: 1 0 auto; 74 | width: 100%; 75 | } 76 | 77 | .carousel_items img { 78 | width: 100%; 79 | display: block; 80 | } 81 | 82 | /* next prev arrow selectors */ 83 | .carousel_nextprev-disabled, 84 | .carousel-hide-nav .carousel_nextprev, 85 | .carousel-hide-nav .carousel_nav { 86 | opacity: 0.3; 87 | cursor: default; 88 | } 89 | 90 | .carousel_nav, 91 | .carousel_nav_inner { 92 | position: relative; 93 | margin: 1em 0; 94 | overflow: auto; 95 | -webkit-overflow-scrolling: touch; 96 | display: flex; 97 | justify-content: flex-start; 98 | width: 100%; 99 | gap: 10px; 100 | } 101 | 102 | .carousel_nav a { 103 | overflow: hidden; 104 | border: 5px solid #fff; 105 | white-space: normal; 106 | flex: 0 0 auto; 107 | display: block; 108 | vertical-align: middle; 109 | height: 50px; 110 | width: auto; 111 | margin: 0; 112 | } 113 | /* disabled state comes from pagination mode */ 114 | .carousel_nav a[disabled="true"] { 115 | display: none !important; 116 | } 117 | .carousel_nav a.carousel_nav_item-selected { 118 | /* selected styles here */ 119 | border-color: lightblue; 120 | } 121 | 122 | .carousel_nav img { 123 | display: block; 124 | height: 100%; 125 | width: auto; 126 | max-width: 100%; 127 | } 128 | 129 | /* features */ 130 | 131 | /* next prev arrow selectors */ 132 | .carousel_nextprev, 133 | .carousel_nextprev_item { 134 | list-style: none; 135 | margin: 0; 136 | padding: 0; 137 | } 138 | .carousel_nextprev_next, 139 | .carousel_nextprev_prev { 140 | position: absolute; 141 | top: 50%; 142 | width: 46px; 143 | height: 46px; 144 | line-height: 46px; 145 | margin-top: -23px; 146 | background-color: #fff; 147 | border-radius: 100%; 148 | overflow: hidden; 149 | text-align: center; 150 | font-size: 0.7em; 151 | text-transform: uppercase; 152 | text-decoration: none; 153 | border: 1px solid #eee; 154 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); 155 | } 156 | .carousel_nextprev_next:before, 157 | .carousel_nextprev_prev:before { 158 | content: ""; 159 | position: absolute; 160 | width: 1em; 161 | height: 1em; 162 | border-right: 4px solid currentColor; 163 | border-bottom: 4px solid currentColor; 164 | transform: rotate(-45deg); 165 | transform-origin: 0 0; 166 | margin-left: calc(-1em + -2px); 167 | } 168 | .carousel_nextprev_prev:before { 169 | transform: rotate(135deg); 170 | margin-left:calc( 1em + 2px); 171 | } 172 | .carousel_nextprev_next:not(.carousel_nextprev-disabled), 173 | .carousel_nextprev_prev:not(.carousel_nextprev-disabled) { 174 | opacity: 0.8; 175 | cursor: pointer; 176 | } 177 | .carousel_nextprev_next:not(.carousel_nextprev-disabled):hover, 178 | .carousel_nextprev_next:not(.carousel_nextprev-disabled):focus, 179 | .carousel_nextprev_prev:not(.carousel_nextprev-disabled):hover, 180 | .carousel_nextprev_prev:not(.carousel_nextprev-disabled):focus { 181 | opacity: 1; 182 | } 183 | .carousel_nextprev_next { 184 | right: -23px; 185 | } 186 | .carousel_nextprev_prev { 187 | left: -23px; 188 | } 189 | 190 | @media (min-width: 40em) { 191 | .carousel_nextprev_next, 192 | .carousel_nextprev_prev { 193 | width: 50px; 194 | height: 50px; 195 | line-height: 50px; 196 | margin-top: -25px; 197 | } 198 | .carousel_nextprev_next { 199 | right: -25px; 200 | } 201 | .carousel_nextprev_prev { 202 | left: -25px; 203 | } 204 | } 205 | 206 | /* dots nav */ 207 | .carousel_nav-dots { 208 | display: block; 209 | margin: 1em 0; 210 | text-align: center; 211 | } 212 | .carousel_nav.carousel_nav-dots a { 213 | display: inline-block; 214 | width: 10px; 215 | height: 10px; 216 | margin-right: 10px; 217 | background: #ccc; 218 | border: 0; 219 | border-radius: 100%; 220 | overflow: hidden; 221 | text-indent: -9999px; 222 | cursor: pointer; 223 | } 224 | .carousel_nav-dots a.carousel_nav_item-selected { 225 | background: #111; 226 | border: none; 227 | box-shadow: none; 228 | } 229 | -------------------------------------------------------------------------------- /test/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.11.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-carousel: carousel; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } 245 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | /* 3 | ======== A Handy Little QUnit Reference ======== 4 | http://api.qunitjs.com/ 5 | 6 | Test methods: 7 | module(name, {[setup][ ,teardown]}) 8 | test(name, callback) 9 | expect(numberOfAssertions) 10 | stop(increment) 11 | start(decrement) 12 | Test assertions: 13 | ok(value, [message]) 14 | equal(actual, expected, [message]) 15 | notEqual(actual, expected, [message]) 16 | deepEqual(actual, expected, [message]) 17 | notDeepEqual(actual, expected, [message]) 18 | strictEqual(actual, expected, [message]) 19 | notStrictEqual(actual, expected, [message]) 20 | throws(block, [expected], [message]) 21 | */ 22 | 23 | window.onload = function(){ 24 | 25 | var carouselA = document.querySelector("fg-carousel"); 26 | 27 | 28 | 29 | test( "general API checks", function(){ 30 | ok( customElements.get("fg-carousel"), "carousel custom element class is defined" ); 31 | 32 | ok( carouselA.classList.contains("carousel"), "carousel one has carousel class" ) 33 | 34 | ok( carouselA.connectedCallback, "carouselA connected callback is defined") 35 | ok( carouselA.disconnectedCallback, "carouselA disconnected callback is defined") 36 | 37 | }); 38 | 39 | 40 | 41 | asyncTest( 'Enhancement steps', function() { 42 | start(); 43 | ok(carouselA.querySelector(".carousel_nextprev"), "next prev generated"); 44 | ok(carouselA.querySelectorAll(".carousel_nextprev button").length === 2, "2 next prev links"); 45 | }); 46 | 47 | 48 | asyncTest( 'Snapping occurs after scrolling to a spot that is not a snap point', function() { 49 | expect(1); 50 | carouselA.querySelector(".carousel_pane").scrollLeft = 0; 51 | carouselA.querySelector(".carousel_pane").scrollLeft = 35; 52 | setTimeout(function(){ 53 | ok( carouselA.querySelector(".carousel_pane").scrollLeft ===0 ); 54 | start(); 55 | },1000); 56 | }); 57 | 58 | 59 | 60 | asyncTest( 'thumbnail clicks cause pane to scroll', function() { 61 | expect(1); 62 | carouselA.querySelector(".carousel_pane").scrollLeft = 500; 63 | carouselA.querySelector(".carousel_nav a").click(); 64 | setTimeout(function(){ 65 | ok( carouselA.querySelector(".carousel_pane").scrollLeft !== 500, "scroll changed" ); 66 | start(); 67 | },1000); 68 | }); 69 | 70 | 71 | asyncTest( 'disabled arrow classes are present at extremes', function() { 72 | expect(4); 73 | 74 | 75 | setTimeout(function(){ 76 | ok( !carouselA.querySelector(".carousel_nextprev_prev.carousel_nextprev-disabled"), "prev link is not disabled "); 77 | ok( carouselA.querySelector(".carousel_nextprev_next.carousel_nextprev-disabled"), "next link is disabled "); 78 | start(); 79 | },2000); 80 | 81 | ok( carouselA.querySelector(".carousel_nextprev_prev.carousel_nextprev-disabled"), "prev link is disabled "); 82 | ok( !carouselA.querySelector(".carousel_nextprev_next.carousel_nextprev-disabled"), "next link is not disabled "); 83 | 84 | carouselA.querySelector(".carousel_pane").scrollLeft = 500; 85 | }); 86 | 87 | 88 | asyncTest( 'Arrows navigate', function() { 89 | expect(1); 90 | carouselA.querySelector(".carousel_pane").scrollLeft = 0; 91 | 92 | setTimeout(function(){ 93 | ok( carouselA.querySelector(".carousel_pane").scrollLeft !== 0, "scroll changed" ); 94 | start(); 95 | },2000); 96 | setTimeout(() => { 97 | carouselA.querySelector(".carousel_nextprev_next").click(); 98 | }, 1000); 99 | }); 100 | 101 | 102 | asyncTest( 'Arrows navigate back', function() { 103 | expect(2); 104 | carouselA.querySelector(".carousel_pane").scrollLeft = 0; 105 | 106 | setTimeout(function(){ 107 | ok( carouselA.querySelector(".carousel_pane").scrollLeft === 0, "scroll changed" ); 108 | start(); 109 | },4000); 110 | 111 | setTimeout(function(){ 112 | ok( carouselA.querySelector(".carousel_pane").scrollLeft !== 0, "scroll changed" ); 113 | carouselA.querySelector(".carousel_nextprev_prev").click(); 114 | },2000); 115 | 116 | setTimeout(function(){ 117 | carouselA.querySelector(".carousel_nextprev_next").click(); 118 | }, 1000); 119 | }); 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | asyncTest( 'random # link clicks are ignored', function() { 129 | expect(1); 130 | carouselA.querySelector("#testlink").trigger( "click" ); 131 | ok( true ); 132 | start(); 133 | }); 134 | 135 | /* 136 | 137 | asyncTest( 'get index returns correct index after goto', function(){ 138 | expect(1); 139 | 140 | 141 | setTimeout(function(){ 142 | equal(carouselA.querySelectorcarousel.carousel("getIndex"), 1); 143 | start(); 144 | }, 2000); 145 | 146 | carouselA.querySelectorcarousel.carousel("goto", 1); 147 | }); 148 | 149 | asyncTest( 'autoplay advances a few times once started', function(){ 150 | expect(2); 151 | var eventCounter = 0; 152 | var checkBinding; 153 | var carouselA.querySelectorcarouselElem = carouselA.querySelector(".carousel"); 154 | var carouselA.querySelectorcarousel; 155 | 156 | carouselA.querySelectorcarouselElem.attr( "data-carousel-autoplay", "500" ); 157 | 158 | carouselA.querySelectorcarouselElem.bind("carousel.after-goto", checkBinding = function(){ 159 | ok(true, "after-goto called"); 160 | 161 | if(++eventCounter === 2){ 162 | carouselA.querySelectorcarouselElem.removeAttr( "data-carousel-autoplay" ); 163 | carouselA.querySelector(document).unbind("carousel.after-goto", checkBinding); 164 | start(); 165 | } 166 | }); 167 | 168 | }); 169 | 170 | asyncTest( 'looping goes endlessly forward', function(){ 171 | expect(5); 172 | var eventCounter = 0; 173 | var checkBinding; 174 | var carouselA.querySelectorcarouselElem = carouselA.querySelector(".carousel"); 175 | var carouselA.querySelectorcarousel; 176 | 177 | carouselA.querySelectorcarouselElem.attr( "data-carousel-loop", "500" ); 178 | 179 | carouselA.querySelectorcarouselElem.bind("carousel.after-next", checkBinding = function(){ 180 | ok(true, "after-next called"); 181 | 182 | if(++eventCounter === 5){ 183 | carouselA.querySelectorcarouselElem.removeAttr( "data-carousel-loop" ); 184 | carouselA.querySelector(document).unbind("carousel.after-goto", checkBinding); 185 | start(); 186 | } 187 | else{ 188 | carouselA.querySelector(".carousel_nextprev_next").click(); 189 | } 190 | }); 191 | 192 | carouselA.querySelectorcarouselElem.carousel(); 193 | setTimeout(() => { 194 | carouselA.querySelector(".carousel_nextprev_next").click(); 195 | }, 500); 196 | }); 197 | 198 | 199 | asyncTest( 'looping goes endlessly in reverse', function(){ 200 | expect(5); 201 | var eventCounter = 0; 202 | var checkBinding; 203 | var carouselA.querySelectorcarouselElem = carouselA.querySelector(".carousel"); 204 | var carouselA.querySelectorcarousel; 205 | 206 | carouselA.querySelectorcarouselElem.attr( "data-carousel-loop", "500" ); 207 | 208 | carouselA.querySelectorcarouselElem.bind("carousel.after-prev", checkBinding = function(){ 209 | ok(true, "after-prev called"); 210 | 211 | if(++eventCounter === 5){ 212 | carouselA.querySelectorcarouselElem.removeAttr( "data-carousel-loop" ); 213 | carouselA.querySelector(document).unbind("carousel.after-goto", checkBinding); 214 | start(); 215 | } 216 | else{ 217 | carouselA.querySelector(".carousel_nextprev_prev").click(); 218 | } 219 | }); 220 | 221 | carouselA.querySelectorcarouselElem.carousel(); 222 | setTimeout(() => { 223 | carouselA.querySelector(".carousel_nextprev_prev").click(); 224 | }, 500); 225 | }); 226 | 227 | */ 228 | 229 | }; 230 | 231 | 232 | 233 | 234 | 235 | 236 | }(window)); 237 | -------------------------------------------------------------------------------- /demo/lib/document-register-element.js: -------------------------------------------------------------------------------- 1 | /*! (C) Andrea Giammarchi - @WebReflection - ISC Style License */ 2 | !function(e,t){"use strict";function n(){var e=A.splice(0,A.length);for(et=0;e.length;)e.shift().call(null,e.shift())}function r(e,t){for(var n=0,r=e.length;n1)&&H(this)}}}),_e(l,j,{value:function(e){-1>0,_="__"+R+U,k="addEventListener",x="attached",q="Callback",B="detached",Z="extends",j="attributeChanged"+q,G=x+q,z="connected"+q,K="disconnected"+q,X="created"+q,$=B+q,Q="ADDITION",W="REMOVAL",Y="DOMAttrModified",J="DOMContentLoaded",ee="DOMSubtreeModified",te="<",ne="=",re=/^[A-Z][._A-Z0-9]*-[-._A-Z0-9]*$/,oe=["ANNOTATION-XML","COLOR-PROFILE","FONT-FACE","FONT-FACE-SRC","FONT-FACE-URI","FONT-FACE-FORMAT","FONT-FACE-NAME","MISSING-GLYPH"],le=[],ae=[],ie="",ue=y.documentElement,ce=le.indexOf||function(e){for(var t=this.length;t--&&this[t]!==e;);return t},se=C.prototype,me=se.hasOwnProperty,fe=se.isPrototypeOf,de=C.defineProperty,pe=[],he=C.getOwnPropertyDescriptor,Te=C.getOwnPropertyNames,Le=C.getPrototypeOf,Me=C.setPrototypeOf,Ee=!!C.__proto__,ve=!1,He="__dreCEv1",ge=e.customElements,be=!/^force/.test(t.type)&&!!(ge&&ge.define&&ge.get&&ge.whenDefined),ye=C.create||C,Ce=e.Map||function(){var e,t=[],n=[];return{get:function(e){return n[ce.call(t,e)]},set:function(r,o){e=ce.call(t,r),e<0?n[t.push(r)-1]=o:n[e]=o}}},we=e.Promise||function(e){function t(e){for(r=!0;n.length;)n.shift()(e)}var n=[],r=!1,o={"catch":function(){return o},then:function(e){return n.push(e),r&&setTimeout(t,1),o}};return e(t),o},Ae=!1,Oe=ye(null),Ne=ye(null),De=new Ce,Ie=function(e){return e.toLowerCase()},Fe=C.create||function ct(e){return e?(ct.prototype=e,new ct):this},Se=Me||(Ee?function(e,t){return e.__proto__=t,e}:Te&&he?function(){function e(e,t){for(var n,r=Te(t),o=0,l=r.length;o
",new Ve(function(e,t){if(e[0]&&"childList"==e[0].type&&!e[0].removedNodes[0].childNodes.length){P=he(Re,"innerHTML");var n=P&&P.set;n&&de(Re,"innerHTML",{set:function(e){for(;this.lastChild;)this.removeChild(this.lastChild);n.call(this,e)}})}t.disconnect(),P=null}).observe(P,{childList:!0,subtree:!0}),P.innerHTML=""),tt||(Me||Ee?(S=function(e,t){fe.call(t,e)||d(e,t)},V=d):(S=function(e,t){e[_]||(e[_]=C(!0),d(e,t))},V=S),Ue?(ot=!1,function(){var e=he(Re,k),t=e.value,n=function(e){var t=new CustomEvent(Y,{bubbles:!0});t.attrName=e,t.prevValue=Ge.call(this,e),t.newValue=null,t[W]=t.attrChange=2,Ke.call(this,e),je.call(this,t)},r=function(e,t){var n=ze.call(this,e),r=n&&Ge.call(this,e),o=new CustomEvent(Y,{bubbles:!0});Xe.call(this,e,t),o.attrName=e,o.prevValue=n?r:null,o.newValue=t,n?o.MODIFICATION=o.attrChange=1:o[Q]=o.attrChange=0,je.call(this,o)},o=function(e){var t,n=e.currentTarget,r=n[_],o=e.propertyName;r.hasOwnProperty(o)&&(r=r[o],t=new CustomEvent(Y,{bubbles:!0}),t.attrName=r.name,t.prevValue=r.value||null,t.newValue=r.value=n[o]||null,null==t.prevValue?t[Q]=t.attrChange=0:t.MODIFICATION=t.attrChange=1,je.call(n,t))};e.value=function(e,l,a){e===Y&&this[j]&&this.setAttribute!==r&&(this[_]={className:{name:"class",value:this.className}},this.setAttribute=r,this.removeAttribute=n,t.call(this,"propertychange",o)),t.call(this,e,l,a)},de(Re,k,e)}()):Ve||(ue[k](Y,Je),ue.setAttribute(_,1),ue.removeAttribute(_),ot&&(O=function(e){var t,n,r,o=this;if(o===e.target){t=o[_],o[_]=n=D(o);for(r in n){if(!(r in t))return N(0,o,r,t[r],n[r],Q);if(n[r]!==t[r])return N(1,o,r,t[r],n[r],"MODIFICATION")}for(r in t)if(!(r in n))return N(2,o,r,t[r],n[r],W)}},N=function(e,t,n,r,o,l){var a={attrChange:e,currentTarget:t,attrName:n,prevValue:r,newValue:o};a[l]=e,u(a)},D=function(e){for(var t,n,r={},o=e.attributes,l=0,a=o.length;l$");if(n[Z]="a",t.prototype=Fe(Pe.prototype),t.prototype.constructor=t,e.customElements.define(r,t,n),!o.test(y.createElement("a",{is:r}).outerHTML)||!o.test((new t).outerHTML))throw n}(function st(){return Reflect.construct(Pe,[],st)},{},"document-register-element-a"+U)}catch(it){b()}if(!t.noBuiltIn)try{if($e.call(y,"a","a").outerHTML.indexOf("is")<0)throw{}}catch(ut){Ie=function(e){return{is:e.toLowerCase()}}}}(window); 3 | -------------------------------------------------------------------------------- /src/fg-carousel.js: -------------------------------------------------------------------------------- 1 | 2 | class carousel extends HTMLElement { 3 | constructor(){ 4 | super(); 5 | this._init = this._init.bind(this); 6 | this._observer = new MutationObserver(this._init); 7 | } 8 | connectedCallback(){ 9 | if (this.children.length) { 10 | this._init(); 11 | } 12 | this._observer.observe(this, { childList: true }); 13 | } 14 | _init(){ 15 | this.pluginName = "carousel"; 16 | var self = this; 17 | this.navActiveClass = this.pluginName + "_nav_item-selected"; 18 | this.activeItemClass = this.pluginName + "_item-active"; 19 | 20 | this.initEvent = this.makeEvent("init"); 21 | this.activeEvent = this.makeEvent("active"); 22 | this.inActiveEvent = this.makeEvent("inactive"); 23 | this.interacted = false; 24 | 25 | this.idItems(); 26 | this.observeItems(); 27 | this.defineElems(); 28 | 29 | if( this.hasAttribute( "data-carousel-nextprev") ){ 30 | this.addNextPrev(); 31 | setTimeout(function(){ 32 | self.manageArrowState(); 33 | }); 34 | } 35 | 36 | this.paginated = this.hasAttribute( "data-carousel-paginated" ); 37 | 38 | if( this.paginated ){ 39 | this.manageDynamicNav(); 40 | } 41 | 42 | this.bindEvents(); 43 | 44 | this.dispatchEvent( this.initEvent ); 45 | 46 | this.autoplayAttr = this.getAttribute( "data-carousel-autoplay"); 47 | if( this.autoplayAttr !== null ){ 48 | setTimeout(function(){ 49 | self.nextAutoplay(); 50 | }, 0); 51 | } 52 | // make sure changes to the item list are tracked 53 | this._observeItemChanges = new MutationObserver( function(){ 54 | self.updateIntObserveList(); 55 | } ); 56 | this._observeItemChanges.observe(this, { childList: true, subtree: true }); 57 | } 58 | makeEvent( evtName ){ 59 | if( typeof window.CustomEvent === "function" ){ 60 | return new CustomEvent( evtName, { 61 | bubbles: true, 62 | cancelable: false 63 | }); 64 | } else { 65 | var evt = document.createEvent('CustomEvent'); 66 | evt.initCustomEvent( evtName, true, true, {} ); 67 | return evt; 68 | } 69 | } 70 | 71 | addNextPrev(){ 72 | var nextprev = document.createElement( "ul" ); 73 | nextprev.classList.add("carousel_nextprev"); 74 | nextprev.innerHTML = ` 75 | 76 | 77 | `; 78 | var nextprevContain = this.querySelector( "." + this.pluginName + "_nextprev_contain" ); 79 | if( !nextprevContain ){ 80 | nextprevContain = this; 81 | } 82 | nextprevContain.append( nextprev ); 83 | } 84 | 85 | defineElems(){ 86 | this.classList.add( this.pluginName ); 87 | this.slider = this.querySelector( ".carousel_pane" ); 88 | this.nav = this.querySelector( ".carousel_nav" ); 89 | if( this.nav ){ 90 | this.nav.setAttribute('role', 'tablist'); 91 | } 92 | this.nextprev = this.querySelector( ".carousel_nextprev" ); 93 | } 94 | 95 | 96 | observerCallback( entries ){ 97 | var self = this; 98 | var parentElem = this; 99 | var navElem = this.nav; 100 | entries.forEach(function( entry ){ 101 | var entryNavLink = parentElem.querySelector( "a[href='#" + entry.target.id + "']" ); 102 | if (entry.isIntersecting && entry.intersectionRatio >= .75 ) { 103 | entry.target.classList.add( self.activeItemClass ); 104 | entry.target.inert = false; 105 | entry.target.setAttribute("role", 'tabpanel'); 106 | entry.target.setAttribute("tabindex", '0'); 107 | entry.target.setAttribute("aria-hidden", 'false'); 108 | let panelId = entry.target.getAttribute('id'); 109 | let tabId = panelId + "-tab"; 110 | entry.target.setAttribute("aria-labelledby", tabId); 111 | 112 | entry.target.dispatchEvent( self.activeEvent ); 113 | if( navElem && entryNavLink ){ 114 | entryNavLink.classList.add( self.navActiveClass ); 115 | entryNavLink.setAttribute("role", "tab"); 116 | entryNavLink.setAttribute("tabindex", "0"); 117 | entryNavLink.setAttribute("id", tabId); 118 | entryNavLink.setAttribute("aria-controls", panelId ); 119 | if( navElem.scrollTo ){ 120 | navElem.scrollTo({ left: entryNavLink.offsetLeft, behavior: "smooth" }); 121 | } 122 | else { 123 | navElem.scrollLeft = entryNavLink.offsetLeft; 124 | } 125 | } 126 | } 127 | else { 128 | entry.target.classList.remove( self.pluginName + "_item-active" ); 129 | entry.target.setAttribute("role", 'tabpanel'); 130 | entry.target.inert = true; 131 | entry.target.setAttribute("tabindex", '-1'); 132 | entry.target.setAttribute("aria-hidden", 'true'); 133 | let panelId = entry.target.getAttribute('id'); 134 | let tabId = panelId + "-tab"; 135 | entry.target.setAttribute("aria-labelledby", tabId); 136 | entry.target.dispatchEvent( self.inActiveEvent ); 137 | if( entryNavLink ){ 138 | entryNavLink.classList.remove( self.navActiveClass ); 139 | entryNavLink.setAttribute("role", "tab"); 140 | entryNavLink.setAttribute("aria-controls", panelId ); 141 | entryNavLink.setAttribute("tabindex", "-1" ); 142 | entryNavLink.setAttribute("id", tabId); 143 | } 144 | } 145 | }); 146 | } 147 | 148 | getItems(){ 149 | return this.querySelectorAll( "." + this.pluginName + "_item" ); 150 | } 151 | 152 | idItems(){ 153 | var self = this; 154 | this.getItems().forEach(function( item ){ 155 | if( !item.id ){ 156 | item.id = self.pluginName + "-" + new Date().getTime(); 157 | } 158 | }); 159 | } 160 | 161 | observeItems(){ 162 | var self=this; 163 | this._intObserver = new IntersectionObserver(function( entries ){ 164 | self.observerCallback( entries ); 165 | }, {root: self, threshold: .75 }); 166 | this.updateIntObserveList(); 167 | this._intObserver.takeRecords(); 168 | } 169 | 170 | updateIntObserveList(){ 171 | var self = this; 172 | this.querySelectorAll( "." + this.pluginName + "_item" ).forEach(function( item ){ 173 | self._intObserver.observe( item ); 174 | }); 175 | } 176 | 177 | 178 | // get the carousel_item elements whose left offsets fall within the scroll pane. 179 | activeItems(){ 180 | return this.querySelectorAll( "." + this.activeItemClass ); 181 | } 182 | 183 | // sort an item to either end to ensure there's always something to advance to 184 | updateSort() { 185 | if( this.loopDisabled || !this.closest( "[data-carousel-loop]" ) ){ 186 | return; 187 | } 188 | var scrollWidth = this.slider.scrollWidth; 189 | var scrollLeft = this.slider.scrollLeft; 190 | var contain = this.querySelector( "." + this.pluginName + "_items" ); 191 | var items = this.querySelectorAll( "." + this.pluginName + "_item" ); 192 | var width = this.offsetWidth; 193 | 194 | if (scrollLeft < width ) { 195 | var sortItem = items[ items.length - 1 ]; 196 | var sortItemWidth = sortItem.offsetWidth; 197 | contain.prepend(sortItem); 198 | this.slider.scrollLeft = scrollLeft + sortItemWidth; 199 | } 200 | else if (scrollWidth - scrollLeft - width <= 0 ) { 201 | var sortItem = items[0]; 202 | var sortItemWidth = sortItem.offsetWidth; 203 | contain.append(sortItem); 204 | this.slider.scrollLeft = scrollLeft - sortItemWidth; 205 | } 206 | } 207 | 208 | 209 | 210 | bindEvents(){ 211 | var self = this; 212 | // clicks for thumbs, nav 213 | this.addEventListener("click", function( e ){ 214 | self.handleClick( e ); 215 | self.interacted = true; 216 | } ); 217 | 218 | // keyboard arrows 219 | this.addEventListener("keydown", function( e ){ 220 | self.keydownHandler( e ); 221 | } ); 222 | 223 | // autoplay stops 224 | this.addEventListener("click", function( e ){ 225 | self.interacted = true; 226 | self.stopAutoplay(); 227 | }); 228 | 229 | 230 | this.addEventListener("pointerdown", function( e ){ 231 | self.interacted = true; 232 | self.stopAutoplay(); 233 | }); 234 | this.addEventListener("focus", function( e ){ 235 | self.interacted = true; 236 | self.stopAutoplay(); 237 | }); 238 | this.slider.addEventListener( "focus", function(){ 239 | self.loopDisabled = true; 240 | }); 241 | 242 | // cleanup on resize 243 | if( this.hasAttribute( "data-carousel-nextprev") ){ 244 | window.addEventListener("resize", function( e ){ 245 | self.manageArrowState(); 246 | }); 247 | } 248 | 249 | if( this.paginated ){ 250 | window.addEventListener("resize", function( e ){ 251 | self.manageDynamicNav(); 252 | }); 253 | } 254 | 255 | var scrolling; 256 | this.slider.addEventListener("scroll", function( e ){ 257 | clearTimeout(scrolling); 258 | scrolling = setTimeout(function(){ 259 | self.updateSort(); 260 | if( self.hasAttribute( "data-carousel-nextprev") ){ 261 | self.manageArrowState(); 262 | } 263 | },66); 264 | }); 265 | 266 | setTimeout(function(){ 267 | self.updateSort(); 268 | }); 269 | 270 | 271 | } 272 | 273 | resizeRetain(){ 274 | var afterResize; 275 | var self = this; 276 | var currSlide; 277 | function resizeUpdates(){ 278 | clearTimeout( afterResize ); 279 | if( !currSlide ){ 280 | currSlide = self.activeItems()[0]; 281 | } 282 | afterResize = setTimeout( function(){ 283 | // retain snapping on resize 284 | self.goto( currSlide ); 285 | currSlide = null; 286 | // resize can reveal or hide slides, so update arrows 287 | self.manageArrowState(); 288 | }, 300 ); 289 | } 290 | 291 | window.addEventListener("resize", resizeUpdates); 292 | 293 | } 294 | 295 | manageDynamicNav(){ 296 | var pane = this.slider; 297 | var scrollWidth = pane.scrollWidth; 298 | var width = pane.offsetWidth; 299 | var regions = scrollWidth / width; 300 | //this.setAttribute( "data-carousel-pages", regions.toFixed(3) ) 301 | var allSlides = this.getItems(); 302 | var allThumbs = this.nav.querySelectorAll("a"); 303 | this.iterator = Math.round( allSlides.length / regions ); 304 | for( var i = 0; i < allSlides.length; i++ ){ 305 | allThumbs[i].setAttribute("disabled", true) 306 | } 307 | for( var i = 0; i < allSlides.length; i+=this.iterator ){ 308 | allThumbs[i].removeAttribute("disabled") 309 | } 310 | } 311 | 312 | manageArrowState(){ 313 | // old api helper here. 314 | if( this.closest( "[data-carousel-loop], [data-loop]" ) ){ 315 | return; 316 | } 317 | var pane = this.slider; 318 | var nextLink = this.querySelector("." + this.pluginName + "_nextprev_next"); 319 | var prevLink = this.querySelector("." + this.pluginName + "_nextprev_prev"); 320 | var currScroll = pane.scrollLeft; 321 | var scrollWidth = pane.scrollWidth; 322 | var width = pane.offsetWidth; 323 | 324 | var noScrollAvailable = (width === scrollWidth); 325 | 326 | var maxScroll = scrollWidth - width; 327 | if (currScroll >= maxScroll - 3 || noScrollAvailable ) { // 3 here is arbitrary tolerance 328 | nextLink.classList.add("carousel_nextprev-disabled"); 329 | nextLink.setAttribute("disabled", true); 330 | } else { 331 | nextLink.classList.remove("carousel_nextprev-disabled"); 332 | nextLink.removeAttribute("disabled"); 333 | } 334 | 335 | if (currScroll > 3 && !noScrollAvailable ) { // 3 is arbitrary tolerance 336 | prevLink.classList.remove("carousel_nextprev-disabled"); 337 | prevLink.removeAttribute("disabled"); 338 | } else { 339 | prevLink.classList.add("carousel_nextprev-disabled"); 340 | prevLink.setAttribute("disabled", true); 341 | } 342 | 343 | if( noScrollAvailable ){ 344 | this.classList.add( "carousel-hide-nav" ); 345 | } 346 | else { 347 | this.classList.remove( "carousel-hide-nav" ); 348 | } 349 | } 350 | 351 | handleClick( e ){ 352 | var self = this; 353 | 354 | var parentAnchor = e.target.closest( "a" ); 355 | if( e.target.closest( ".carousel_nextprev_next" ) ){ 356 | e.preventDefault(); 357 | return self.arrowNavigate( true ); 358 | } 359 | else if( e.target.closest( ".carousel_nextprev_prev" ) ){ 360 | e.preventDefault(); 361 | return self.arrowNavigate( false ); 362 | } 363 | // internal links to slides 364 | else if( parentAnchor ){ 365 | e.preventDefault(); 366 | self.goto( parentAnchor.getAttribute("href") ); 367 | } 368 | } 369 | 370 | 371 | nextAutoplay(){ 372 | var currentActive = this.activeItems()[0]; 373 | var self = this; 374 | if(currentActive){ 375 | var autoTiming = currentActive.getAttribute( "data-carousel-autoplay" ) || this.autoplayAttr; 376 | if( autoTiming !== null ){ 377 | if( autoTiming ) { 378 | var thisTime = parseInt(autoTiming, 10) || 5000; 379 | self.autoTiming = setTimeout( function(){ 380 | self.next(); 381 | self.nextAutoplay(); 382 | }, thisTime ); 383 | } 384 | } 385 | } 386 | } 387 | 388 | stopAutoplay(){ 389 | clearTimeout(this.autoTiming); 390 | } 391 | 392 | goto(item, parent, callback){ 393 | var slide; 394 | var focused = document.activeElement; 395 | if( !parent ){ 396 | parent = this.slider; 397 | } 398 | if( typeof(item) === "string" ){ 399 | //go to ID 400 | slide = this.querySelector( item ); 401 | } 402 | else if( typeof(item) === "number" ){ 403 | //go to index 404 | slide = this.getItems()[ item ] 405 | } 406 | else{ 407 | //go to obj 408 | slide = item 409 | } 410 | if( slide ){ 411 | parent.scrollTo({ left: slide.offsetLeft, behavior: "smooth" }); 412 | if( self.interacted && (focused && focused.closest( ".carousel_nextprev, .carousel_items" ) || document.activeElement === document.body ) ){ 413 | setTimeout(function(){ 414 | slide.focus(); 415 | }, 1000); 416 | } 417 | if( callback ){ 418 | callback(); 419 | } 420 | 421 | } 422 | } 423 | 424 | keydownHandler( e ){ 425 | var self = this; 426 | if( e.keyCode === 37 || e.keyCode === 38 ){ 427 | this.stopAutoplay(); 428 | e.preventDefault(); 429 | e.stopImmediatePropagation(); 430 | this.arrowNavigate( false ); 431 | if( e.target.hasAttribute("role", 'tab') && e.target.previousElementSibling && self.interacted ){ 432 | e.target.previousElementSibling.focus(); 433 | } 434 | else { 435 | var prevSlide = self.activeItems()[0].previousElementSibling; 436 | if( prevSlide && self.interacted ){ 437 | setTimeout(prevSlide.focus, 1000); 438 | } 439 | } 440 | } 441 | if( e.keyCode === 39 || e.keyCode === 40 ){ 442 | this.stopAutoplay(); 443 | e.preventDefault(); 444 | e.stopImmediatePropagation(); 445 | this.arrowNavigate( true ); 446 | if( e.target.hasAttribute("role", 'tab') && e.target.nextElementSibling && self.interacted ){ 447 | e.target.nextElementSibling.focus(); 448 | } 449 | else { 450 | var nextSlide = self.activeItems()[0].nextElementSibling; 451 | if( nextSlide && self.interacted ){ 452 | setTimeout(nextSlide.focus, 1000); 453 | } 454 | } 455 | } 456 | } 457 | 458 | 459 | 460 | 461 | // next/prev links or arrows should loop back to the other end when an extreme is reached 462 | arrowNavigate( forward ){ 463 | if( forward ){ 464 | this.next(); 465 | } 466 | else { 467 | this.prev(); 468 | } 469 | } 470 | 471 | 472 | // advance slide one full scrollpane's width forward 473 | next(){ 474 | var currentActive = this.activeItems()[0]; 475 | if(currentActive){ 476 | var next = currentActive.nextElementSibling; 477 | if( this.paginated ){ 478 | var activeNav = this.querySelector("." + this.navActiveClass); 479 | var nextNav = activeNav.nextElementSibling; 480 | while( nextNav.hasAttribute("disabled") ){ 481 | nextNav = nextNav.nextElementSibling; 482 | } 483 | 484 | next = this.querySelector(nextNav.getAttribute('href')) 485 | } 486 | if( next ){ 487 | this.goto( next ); 488 | } 489 | } 490 | } 491 | 492 | // advance slide one full scrollpane's width backwards 493 | prev(){ 494 | var currentActive = this.activeItems()[0]; 495 | if( currentActive ){ 496 | var prev = currentActive.previousElementSibling; 497 | if( this.paginated ){ 498 | var activeNav = this.querySelector("." + this.navActiveClass); 499 | var nextNav = activeNav.previousElementSibling; 500 | while( nextNav.hasAttribute("disabled") ){ 501 | nextNav = nextNav.previousElementSibling; 502 | } 503 | prev = this.querySelector(nextNav.getAttribute('href')) 504 | } 505 | if( prev ){ 506 | this.goto( prev ); 507 | } 508 | } 509 | } 510 | disconnectedCallback(){ 511 | //if needed 512 | } 513 | } 514 | 515 | if ('customElements' in window) { 516 | customElements.define('fg-carousel', carousel ); 517 | } 518 | -------------------------------------------------------------------------------- /demo/es5/fg-carousel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | 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); } } 8 | 9 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 10 | 11 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 12 | 13 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } 14 | 15 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 16 | 17 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 18 | 19 | function _wrapNativeSuper(Class) { var _cache = typeof Map === "function" ? new Map() : undefined; _wrapNativeSuper = function _wrapNativeSuper(Class) { if (Class === null || !_isNativeFunction(Class)) return Class; if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() { return _construct(Class, arguments, _getPrototypeOf(this).constructor); } Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); return _setPrototypeOf(Wrapper, Class); }; return _wrapNativeSuper(Class); } 20 | 21 | function _construct(Parent, args, Class) { if (_isNativeReflectConstruct()) { _construct = Reflect.construct; } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); } 22 | 23 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } 24 | 25 | function _isNativeFunction(fn) { return Function.toString.call(fn).indexOf("[native code]") !== -1; } 26 | 27 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 28 | 29 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 30 | 31 | var carousel = /*#__PURE__*/function (_HTMLElement) { 32 | _inherits(carousel, _HTMLElement); 33 | 34 | var _super = _createSuper(carousel); 35 | 36 | function carousel() { 37 | var _this; 38 | 39 | _classCallCheck(this, carousel); 40 | 41 | _this = _super.call(this); 42 | _this._init = _this._init.bind(_assertThisInitialized(_this)); 43 | _this._observer = new MutationObserver(_this._init); 44 | return _this; 45 | } 46 | 47 | _createClass(carousel, [{ 48 | key: "connectedCallback", 49 | value: function connectedCallback() { 50 | if (this.children.length) { 51 | this._init(); 52 | } 53 | 54 | this._observer.observe(this, { 55 | childList: true 56 | }); 57 | } 58 | }, { 59 | key: "_init", 60 | value: function _init() { 61 | this.pluginName = "carousel"; 62 | var self = this; 63 | this.navActiveClass = this.pluginName + "_nav_item-selected"; 64 | this.activeItemClass = this.pluginName + "_item-active"; 65 | this.initEvent = this.makeEvent("init"); 66 | this.activeEvent = this.makeEvent("active"); 67 | this.inActiveEvent = this.makeEvent("inactive"); 68 | this.interacted = false; 69 | this.idItems(); 70 | this.observeItems(); 71 | this.defineElems(); 72 | 73 | if (this.hasAttribute("data-carousel-nextprev")) { 74 | this.addNextPrev(); 75 | setTimeout(function () { 76 | self.manageArrowState(); 77 | }); 78 | } 79 | 80 | this.paginated = this.hasAttribute("data-carousel-paginated"); 81 | 82 | if (this.paginated) { 83 | this.manageDynamicNav(); 84 | } 85 | 86 | this.bindEvents(); 87 | this.dispatchEvent(this.initEvent); 88 | this.autoplayAttr = this.getAttribute("data-carousel-autoplay"); 89 | 90 | if (this.autoplayAttr !== null) { 91 | setTimeout(function () { 92 | self.nextAutoplay(); 93 | }, 0); 94 | } // make sure changes to the item list are tracked 95 | 96 | 97 | this._observeItemChanges = new MutationObserver(function () { 98 | self.updateIntObserveList(); 99 | }); 100 | 101 | this._observeItemChanges.observe(this, { 102 | childList: true, 103 | subtree: true 104 | }); 105 | } 106 | }, { 107 | key: "makeEvent", 108 | value: function makeEvent(evtName) { 109 | if (typeof window.CustomEvent === "function") { 110 | return new CustomEvent(evtName, { 111 | bubbles: true, 112 | cancelable: false 113 | }); 114 | } else { 115 | var evt = document.createEvent('CustomEvent'); 116 | evt.initCustomEvent(evtName, true, true, {}); 117 | return evt; 118 | } 119 | } 120 | }, { 121 | key: "addNextPrev", 122 | value: function addNextPrev() { 123 | var nextprev = document.createElement("ul"); 124 | nextprev.classList.add("carousel_nextprev"); 125 | nextprev.innerHTML = "\n\t\t\t
  • \n\t\t\t
  • \n\t\t"; 126 | var nextprevContain = this.querySelector("." + this.pluginName + "_nextprev_contain"); 127 | 128 | if (!nextprevContain) { 129 | nextprevContain = this; 130 | } 131 | 132 | nextprevContain.append(nextprev); 133 | } 134 | }, { 135 | key: "defineElems", 136 | value: function defineElems() { 137 | this.classList.add(this.pluginName); 138 | this.slider = this.querySelector(".carousel_pane"); 139 | this.nav = this.querySelector(".carousel_nav"); 140 | 141 | if (this.nav) { 142 | this.nav.setAttribute('role', 'tablist'); 143 | } 144 | 145 | this.nextprev = this.querySelector(".carousel_nextprev"); 146 | } 147 | }, { 148 | key: "observerCallback", 149 | value: function observerCallback(entries) { 150 | var self = this; 151 | var parentElem = this; 152 | var navElem = this.nav; 153 | entries.forEach(function (entry) { 154 | var entryNavLink = parentElem.querySelector("a[href='#" + entry.target.id + "']"); 155 | 156 | if (entry.isIntersecting && entry.intersectionRatio >= .75) { 157 | entry.target.classList.add(self.activeItemClass); 158 | entry.target.inert = false; 159 | entry.target.setAttribute("role", 'tabpanel'); 160 | entry.target.setAttribute("tabindex", '0'); 161 | entry.target.setAttribute("aria-hidden", 'false'); 162 | var panelId = entry.target.getAttribute('id'); 163 | var tabId = panelId + "-tab"; 164 | entry.target.setAttribute("aria-labelledby", tabId); 165 | entry.target.dispatchEvent(self.activeEvent); 166 | 167 | if (navElem && entryNavLink) { 168 | entryNavLink.classList.add(self.navActiveClass); 169 | entryNavLink.setAttribute("role", "tab"); 170 | entryNavLink.setAttribute("tabindex", "0"); 171 | entryNavLink.setAttribute("id", tabId); 172 | entryNavLink.setAttribute("aria-controls", panelId); 173 | 174 | if (navElem.scrollTo) { 175 | navElem.scrollTo({ 176 | left: entryNavLink.offsetLeft, 177 | behavior: "smooth" 178 | }); 179 | } else { 180 | navElem.scrollLeft = entryNavLink.offsetLeft; 181 | } 182 | } 183 | } else { 184 | entry.target.classList.remove(self.pluginName + "_item-active"); 185 | entry.target.setAttribute("role", 'tabpanel'); 186 | entry.target.inert = true; 187 | entry.target.setAttribute("tabindex", '-1'); 188 | entry.target.setAttribute("aria-hidden", 'true'); 189 | 190 | var _panelId = entry.target.getAttribute('id'); 191 | 192 | var _tabId = _panelId + "-tab"; 193 | 194 | entry.target.setAttribute("aria-labelledby", _tabId); 195 | entry.target.dispatchEvent(self.inActiveEvent); 196 | 197 | if (entryNavLink) { 198 | entryNavLink.classList.remove(self.navActiveClass); 199 | entryNavLink.setAttribute("role", "tab"); 200 | entryNavLink.setAttribute("aria-controls", _panelId); 201 | entryNavLink.setAttribute("tabindex", "-1"); 202 | entryNavLink.setAttribute("id", _tabId); 203 | } 204 | } 205 | }); 206 | } 207 | }, { 208 | key: "getItems", 209 | value: function getItems() { 210 | return this.querySelectorAll("." + this.pluginName + "_item"); 211 | } 212 | }, { 213 | key: "idItems", 214 | value: function idItems() { 215 | var self = this; 216 | this.getItems().forEach(function (item) { 217 | if (!item.id) { 218 | item.id = self.pluginName + "-" + new Date().getTime(); 219 | } 220 | }); 221 | } 222 | }, { 223 | key: "observeItems", 224 | value: function observeItems() { 225 | var self = this; 226 | this._intObserver = new IntersectionObserver(function (entries) { 227 | self.observerCallback(entries); 228 | }, { 229 | root: self, 230 | threshold: .75 231 | }); 232 | this.updateIntObserveList(); 233 | 234 | this._intObserver.takeRecords(); 235 | } 236 | }, { 237 | key: "updateIntObserveList", 238 | value: function updateIntObserveList() { 239 | var self = this; 240 | this.querySelectorAll("." + this.pluginName + "_item").forEach(function (item) { 241 | self._intObserver.observe(item); 242 | }); 243 | } // get the carousel_item elements whose left offsets fall within the scroll pane. 244 | 245 | }, { 246 | key: "activeItems", 247 | value: function activeItems() { 248 | return this.querySelectorAll("." + this.activeItemClass); 249 | } // sort an item to either end to ensure there's always something to advance to 250 | 251 | }, { 252 | key: "updateSort", 253 | value: function updateSort() { 254 | if (this.loopDisabled || !this.closest("[data-carousel-loop]")) { 255 | return; 256 | } 257 | 258 | var scrollWidth = this.slider.scrollWidth; 259 | var scrollLeft = this.slider.scrollLeft; 260 | var contain = this.querySelector("." + this.pluginName + "_items"); 261 | var items = this.querySelectorAll("." + this.pluginName + "_item"); 262 | var width = this.offsetWidth; 263 | 264 | if (scrollLeft < width) { 265 | var sortItem = items[items.length - 1]; 266 | var sortItemWidth = sortItem.offsetWidth; 267 | contain.prepend(sortItem); 268 | this.slider.scrollLeft = scrollLeft + sortItemWidth; 269 | } else if (scrollWidth - scrollLeft - width <= 0) { 270 | var sortItem = items[0]; 271 | var sortItemWidth = sortItem.offsetWidth; 272 | contain.append(sortItem); 273 | this.slider.scrollLeft = scrollLeft - sortItemWidth; 274 | } 275 | } 276 | }, { 277 | key: "bindEvents", 278 | value: function bindEvents() { 279 | var self = this; // clicks for thumbs, nav 280 | 281 | this.addEventListener("click", function (e) { 282 | self.handleClick(e); 283 | self.interacted = true; 284 | }); // keyboard arrows 285 | 286 | this.addEventListener("keydown", function (e) { 287 | self.keydownHandler(e); 288 | }); // autoplay stops 289 | 290 | this.addEventListener("click", function (e) { 291 | self.interacted = true; 292 | self.stopAutoplay(); 293 | }); 294 | this.addEventListener("pointerdown", function (e) { 295 | self.interacted = true; 296 | self.stopAutoplay(); 297 | }); 298 | this.addEventListener("focus", function (e) { 299 | self.interacted = true; 300 | self.stopAutoplay(); 301 | }); 302 | this.slider.addEventListener("focus", function () { 303 | self.loopDisabled = true; 304 | }); // cleanup on resize 305 | 306 | if (this.hasAttribute("data-carousel-nextprev")) { 307 | window.addEventListener("resize", function (e) { 308 | self.manageArrowState(); 309 | }); 310 | } 311 | 312 | if (this.paginated) { 313 | window.addEventListener("resize", function (e) { 314 | self.manageDynamicNav(); 315 | }); 316 | } 317 | 318 | var scrolling; 319 | this.slider.addEventListener("scroll", function (e) { 320 | clearTimeout(scrolling); 321 | scrolling = setTimeout(function () { 322 | self.updateSort(); 323 | 324 | if (self.hasAttribute("data-carousel-nextprev")) { 325 | self.manageArrowState(); 326 | } 327 | }, 66); 328 | }); 329 | setTimeout(function () { 330 | self.updateSort(); 331 | }); 332 | } 333 | }, { 334 | key: "resizeRetain", 335 | value: function resizeRetain() { 336 | var afterResize; 337 | var self = this; 338 | var currSlide; 339 | 340 | function resizeUpdates() { 341 | clearTimeout(afterResize); 342 | 343 | if (!currSlide) { 344 | currSlide = self.activeItems()[0]; 345 | } 346 | 347 | afterResize = setTimeout(function () { 348 | // retain snapping on resize 349 | self["goto"](currSlide); 350 | currSlide = null; // resize can reveal or hide slides, so update arrows 351 | 352 | self.manageArrowState(); 353 | }, 300); 354 | } 355 | 356 | window.addEventListener("resize", resizeUpdates); 357 | } 358 | }, { 359 | key: "manageDynamicNav", 360 | value: function manageDynamicNav() { 361 | var pane = this.slider; 362 | var scrollWidth = pane.scrollWidth; 363 | var width = pane.offsetWidth; 364 | var regions = scrollWidth / width; //this.setAttribute( "data-carousel-pages", regions.toFixed(3) ) 365 | 366 | var allSlides = this.getItems(); 367 | var allThumbs = this.nav.querySelectorAll("a"); 368 | this.iterator = Math.round(allSlides.length / regions); 369 | 370 | for (var i = 0; i < allSlides.length; i++) { 371 | allThumbs[i].setAttribute("disabled", true); 372 | } 373 | 374 | for (var i = 0; i < allSlides.length; i += this.iterator) { 375 | allThumbs[i].removeAttribute("disabled"); 376 | } 377 | } 378 | }, { 379 | key: "manageArrowState", 380 | value: function manageArrowState() { 381 | // old api helper here. 382 | if (this.closest("[data-carousel-loop], [data-loop]")) { 383 | return; 384 | } 385 | 386 | var pane = this.slider; 387 | var nextLink = this.querySelector("." + this.pluginName + "_nextprev_next"); 388 | var prevLink = this.querySelector("." + this.pluginName + "_nextprev_prev"); 389 | var currScroll = pane.scrollLeft; 390 | var scrollWidth = pane.scrollWidth; 391 | var width = pane.offsetWidth; 392 | var noScrollAvailable = width === scrollWidth; 393 | var maxScroll = scrollWidth - width; 394 | 395 | if (currScroll >= maxScroll - 3 || noScrollAvailable) { 396 | // 3 here is arbitrary tolerance 397 | nextLink.classList.add("carousel_nextprev-disabled"); 398 | nextLink.setAttribute("disabled", true); 399 | } else { 400 | nextLink.classList.remove("carousel_nextprev-disabled"); 401 | nextLink.removeAttribute("disabled"); 402 | } 403 | 404 | if (currScroll > 3 && !noScrollAvailable) { 405 | // 3 is arbitrary tolerance 406 | prevLink.classList.remove("carousel_nextprev-disabled"); 407 | prevLink.removeAttribute("disabled"); 408 | } else { 409 | prevLink.classList.add("carousel_nextprev-disabled"); 410 | prevLink.setAttribute("disabled", true); 411 | } 412 | 413 | if (noScrollAvailable) { 414 | this.classList.add("carousel-hide-nav"); 415 | } else { 416 | this.classList.remove("carousel-hide-nav"); 417 | } 418 | } 419 | }, { 420 | key: "handleClick", 421 | value: function handleClick(e) { 422 | var self = this; 423 | var parentAnchor = e.target.closest("a"); 424 | 425 | if (e.target.closest(".carousel_nextprev_next")) { 426 | e.preventDefault(); 427 | return self.arrowNavigate(true); 428 | } else if (e.target.closest(".carousel_nextprev_prev")) { 429 | e.preventDefault(); 430 | return self.arrowNavigate(false); 431 | } // internal links to slides 432 | else if (parentAnchor) { 433 | e.preventDefault(); 434 | self["goto"](parentAnchor.getAttribute("href")); 435 | } 436 | } 437 | }, { 438 | key: "nextAutoplay", 439 | value: function nextAutoplay() { 440 | var currentActive = this.activeItems()[0]; 441 | var self = this; 442 | 443 | if (currentActive) { 444 | var autoTiming = currentActive.getAttribute("data-carousel-autoplay") || this.autoplayAttr; 445 | 446 | if (autoTiming !== null) { 447 | if (autoTiming) { 448 | var thisTime = parseInt(autoTiming, 10) || 5000; 449 | self.autoTiming = setTimeout(function () { 450 | self.next(); 451 | self.nextAutoplay(); 452 | }, thisTime); 453 | } 454 | } 455 | } 456 | } 457 | }, { 458 | key: "stopAutoplay", 459 | value: function stopAutoplay() { 460 | clearTimeout(this.autoTiming); 461 | } 462 | }, { 463 | key: "goto", 464 | value: function goto(item, parent, callback) { 465 | var slide; 466 | var focused = document.activeElement; 467 | 468 | if (!parent) { 469 | parent = this.slider; 470 | } 471 | 472 | if (typeof item === "string") { 473 | //go to ID 474 | slide = this.querySelector(item); 475 | } else if (typeof item === "number") { 476 | //go to index 477 | slide = this.getItems()[item]; 478 | } else { 479 | //go to obj 480 | slide = item; 481 | } 482 | 483 | if (slide) { 484 | parent.scrollTo({ 485 | left: slide.offsetLeft, 486 | behavior: "smooth" 487 | }); 488 | 489 | if (self.interacted && (focused && focused.closest(".carousel_nextprev, .carousel_items") || document.activeElement === document.body)) { 490 | setTimeout(function () { 491 | slide.focus(); 492 | }, 1000); 493 | } 494 | 495 | if (callback) { 496 | callback(); 497 | } 498 | } 499 | } 500 | }, { 501 | key: "keydownHandler", 502 | value: function keydownHandler(e) { 503 | var self = this; 504 | 505 | if (e.keyCode === 37 || e.keyCode === 38) { 506 | this.stopAutoplay(); 507 | e.preventDefault(); 508 | e.stopImmediatePropagation(); 509 | this.arrowNavigate(false); 510 | 511 | if (e.target.hasAttribute("role", 'tab') && e.target.previousElementSibling && self.interacted) { 512 | e.target.previousElementSibling.focus(); 513 | } else { 514 | var prevSlide = self.activeItems()[0].previousElementSibling; 515 | 516 | if (prevSlide && self.interacted) { 517 | setTimeout(prevSlide.focus, 1000); 518 | } 519 | } 520 | } 521 | 522 | if (e.keyCode === 39 || e.keyCode === 40) { 523 | this.stopAutoplay(); 524 | e.preventDefault(); 525 | e.stopImmediatePropagation(); 526 | this.arrowNavigate(true); 527 | 528 | if (e.target.hasAttribute("role", 'tab') && e.target.nextElementSibling && self.interacted) { 529 | e.target.nextElementSibling.focus(); 530 | } else { 531 | var nextSlide = self.activeItems()[0].nextElementSibling; 532 | 533 | if (nextSlide && self.interacted) { 534 | setTimeout(nextSlide.focus, 1000); 535 | } 536 | } 537 | } 538 | } // next/prev links or arrows should loop back to the other end when an extreme is reached 539 | 540 | }, { 541 | key: "arrowNavigate", 542 | value: function arrowNavigate(forward) { 543 | if (forward) { 544 | this.next(); 545 | } else { 546 | this.prev(); 547 | } 548 | } // advance slide one full scrollpane's width forward 549 | 550 | }, { 551 | key: "next", 552 | value: function next() { 553 | var currentActive = this.activeItems()[0]; 554 | 555 | if (currentActive) { 556 | var next = currentActive.nextElementSibling; 557 | 558 | if (this.paginated) { 559 | var activeNav = this.querySelector("." + this.navActiveClass); 560 | var nextNav = activeNav.nextElementSibling; 561 | 562 | while (nextNav.hasAttribute("disabled")) { 563 | nextNav = nextNav.nextElementSibling; 564 | } 565 | 566 | next = this.querySelector(nextNav.getAttribute('href')); 567 | } 568 | 569 | if (next) { 570 | this["goto"](next); 571 | } 572 | } 573 | } // advance slide one full scrollpane's width backwards 574 | 575 | }, { 576 | key: "prev", 577 | value: function prev() { 578 | var currentActive = this.activeItems()[0]; 579 | 580 | if (currentActive) { 581 | var prev = currentActive.previousElementSibling; 582 | 583 | if (this.paginated) { 584 | var activeNav = this.querySelector("." + this.navActiveClass); 585 | var nextNav = activeNav.previousElementSibling; 586 | 587 | while (nextNav.hasAttribute("disabled")) { 588 | nextNav = nextNav.previousElementSibling; 589 | } 590 | 591 | prev = this.querySelector(nextNav.getAttribute('href')); 592 | } 593 | 594 | if (prev) { 595 | this["goto"](prev); 596 | } 597 | } 598 | } 599 | }, { 600 | key: "disconnectedCallback", 601 | value: function disconnectedCallback() {//if needed 602 | } 603 | }]); 604 | 605 | return carousel; 606 | }( /*#__PURE__*/_wrapNativeSuper(HTMLElement)); 607 | 608 | if ('customElements' in window) { 609 | customElements.define('fg-carousel', carousel); 610 | } -------------------------------------------------------------------------------- /demo/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

    Demos

    13 | 14 | Quick links to examples: 15 | 16 | - [Standard](#nextprev) 17 | - [Dots](#dots) 18 | - [Varying width breakpoints](#breakpoints) 19 | - [Partial Reveals](#reveal) 20 | - [Fixed width slides](#fixed) 21 | - [Paginated navigation](#pagination) 22 | - [Autoplay](#autoplay) 23 | - [Looping](#looping) 24 | 25 | 26 | 27 |

    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.

    29 | 30 | 31 | 41 | 95 | 96 | 97 | 120 | 121 | 122 | 123 |

    Same Example with Dot Navigation

    124 |

    You can have dots instead of thumbnails by adding the carousel_nav-dots class

    125 |

    You can also set the attribute on carousel_item elements to get individual timing.

    126 | 127 | 137 | 167 | 168 | 169 | 170 | 171 | 198 | 199 |

    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.

    201 | 202 | 203 | 230 | 231 | 232 |

    CSS for this example

    233 |
    
    234 | /* breakpoints example */
    235 | @media (min-width: 40em){
    236 |   .breakpointsexample .carousel_item {
    237 |     width: 50%;
    238 |   }
    239 |   .breakpointsexample .carousel_pane {
    240 |     scroll-snap-points-x: repeat(50%);
    241 |   }
    242 | }
    243 | @media (min-width: 50em){
    244 |   .breakpointsexample .carousel_item {
    245 |     width: 33.333%;
    246 |   }
    247 |   .breakpointsexample .carousel_pane {
    248 |     scroll-snap-points-x: repeat(33.33333%);
    249 |   }
    250 | }
    251 | @media (min-width: 60em){
    252 |   .breakpointsexample .carousel_item {
    253 |     width: 25%;
    254 |   }
    255 |   .breakpointsexample .carousel_pane {
    256 |     scroll-snap-points-x: repeat(25%);
    257 |   }
    258 | }
    259 | 
    260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 278 | 279 |

    Example with partial slide reveals.

    280 | 281 |

    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.

    282 | 283 | 284 | 294 | 324 | 325 | 326 |

    CSS for this example

    327 |
    
    328 |   .revealexample .carousel_item {
    329 |   width: 85%;
    330 |   scroll-snap-align: center;
    331 |   }
    332 |   .revealexample .carousel_pane {
    333 |   scroll-snap-points-x: repeat(85%);
    334 |   }
    335 |   
    336 | 337 | 338 | 339 | 340 | 348 | 349 |

    Fixed width slides

    350 |

    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.

    351 | 352 | 353 | 362 | 389 | 390 | 391 |

    CSS for this example

    392 |
    
    393 |   /* cars example */
    394 | .cars-example .carousel_item {
    395 |   width: 500px;
    396 |   max-width: 100%;
    397 |   scroll-snap-align: center;
    398 | }
    399 |   
    400 | 401 | 402 | 403 | 404 |

    Responsive slide widths with paginated navigation

    405 |

    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.

    406 | 407 | 408 | 417 | 444 | 445 | 446 | 447 | 448 | 449 | 450 |

    Auto-play carousel example

    451 |

    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.

    453 | 454 | 464 | 494 | 495 | 496 | 497 | 498 |

    Example with endless looping

    499 |

    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 | 520 | 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} An array of margin objects with the keys 317 | * value and unit. 318 | */ 319 | IntersectionObserver.prototype._parseRootMargin = function(opt_rootMargin) { 320 | var marginString = opt_rootMargin || '0px'; 321 | var margins = marginString.split(/\s+/).map(function(margin) { 322 | var parts = /^(-?\d*\.?\d+)(px|%)$/.exec(margin); 323 | if (!parts) { 324 | throw new Error('rootMargin must be specified in pixels or percent'); 325 | } 326 | return {value: parseFloat(parts[1]), unit: parts[2]}; 327 | }); 328 | 329 | // Handles shorthand. 330 | margins[1] = margins[1] || margins[0]; 331 | margins[2] = margins[2] || margins[0]; 332 | margins[3] = margins[3] || margins[1]; 333 | 334 | return margins; 335 | }; 336 | 337 | 338 | /** 339 | * Starts polling for intersection changes if the polling is not already 340 | * happening, and if the page's visibility state is visible. 341 | * @param {!Document} doc 342 | * @private 343 | */ 344 | IntersectionObserver.prototype._monitorIntersections = function(doc) { 345 | var win = doc.defaultView; 346 | if (!win) { 347 | // Already destroyed. 348 | return; 349 | } 350 | if (this._monitoringDocuments.indexOf(doc) != -1) { 351 | // Already monitoring. 352 | return; 353 | } 354 | 355 | // Private state for monitoring. 356 | var callback = this._checkForIntersections; 357 | var monitoringInterval = null; 358 | var domObserver = null; 359 | 360 | // If a poll interval is set, use polling instead of listening to 361 | // resize and scroll events or DOM mutations. 362 | if (this.POLL_INTERVAL) { 363 | monitoringInterval = win.setInterval(callback, this.POLL_INTERVAL); 364 | } else { 365 | addEvent(win, 'resize', callback, true); 366 | addEvent(doc, 'scroll', callback, true); 367 | if (this.USE_MUTATION_OBSERVER && 'MutationObserver' in win) { 368 | domObserver = new win.MutationObserver(callback); 369 | domObserver.observe(doc, { 370 | attributes: true, 371 | childList: true, 372 | characterData: true, 373 | subtree: true 374 | }); 375 | } 376 | } 377 | 378 | this._monitoringDocuments.push(doc); 379 | this._monitoringUnsubscribes.push(function() { 380 | // Get the window object again. When a friendly iframe is destroyed, it 381 | // will be null. 382 | var win = doc.defaultView; 383 | 384 | if (win) { 385 | if (monitoringInterval) { 386 | win.clearInterval(monitoringInterval); 387 | } 388 | removeEvent(win, 'resize', callback, true); 389 | } 390 | 391 | removeEvent(doc, 'scroll', callback, true); 392 | if (domObserver) { 393 | domObserver.disconnect(); 394 | } 395 | }); 396 | 397 | // Also monitor the parent. 398 | if (doc != (this.root && this.root.ownerDocument || document)) { 399 | var frame = getFrameElement(doc); 400 | if (frame) { 401 | this._monitorIntersections(frame.ownerDocument); 402 | } 403 | } 404 | }; 405 | 406 | 407 | /** 408 | * Stops polling for intersection changes. 409 | * @param {!Document} doc 410 | * @private 411 | */ 412 | IntersectionObserver.prototype._unmonitorIntersections = function(doc) { 413 | var index = this._monitoringDocuments.indexOf(doc); 414 | if (index == -1) { 415 | return; 416 | } 417 | 418 | var rootDoc = (this.root && this.root.ownerDocument || document); 419 | 420 | // Check if any dependent targets are still remaining. 421 | var hasDependentTargets = 422 | this._observationTargets.some(function(item) { 423 | var itemDoc = item.element.ownerDocument; 424 | // Target is in this context. 425 | if (itemDoc == doc) { 426 | return true; 427 | } 428 | // Target is nested in this context. 429 | while (itemDoc && itemDoc != rootDoc) { 430 | var frame = getFrameElement(itemDoc); 431 | itemDoc = frame && frame.ownerDocument; 432 | if (itemDoc == doc) { 433 | return true; 434 | } 435 | } 436 | return false; 437 | }); 438 | if (hasDependentTargets) { 439 | return; 440 | } 441 | 442 | // Unsubscribe. 443 | var unsubscribe = this._monitoringUnsubscribes[index]; 444 | this._monitoringDocuments.splice(index, 1); 445 | this._monitoringUnsubscribes.splice(index, 1); 446 | unsubscribe(); 447 | 448 | // Also unmonitor the parent. 449 | if (doc != rootDoc) { 450 | var frame = getFrameElement(doc); 451 | if (frame) { 452 | this._unmonitorIntersections(frame.ownerDocument); 453 | } 454 | } 455 | }; 456 | 457 | 458 | /** 459 | * Stops polling for intersection changes. 460 | * @param {!Document} doc 461 | * @private 462 | */ 463 | IntersectionObserver.prototype._unmonitorAllIntersections = function() { 464 | var unsubscribes = this._monitoringUnsubscribes.slice(0); 465 | this._monitoringDocuments.length = 0; 466 | this._monitoringUnsubscribes.length = 0; 467 | for (var i = 0; i < unsubscribes.length; i++) { 468 | unsubscribes[i](); 469 | } 470 | }; 471 | 472 | 473 | /** 474 | * Scans each observation target for intersection changes and adds them 475 | * to the internal entries queue. If new entries are found, it 476 | * schedules the callback to be invoked. 477 | * @private 478 | */ 479 | IntersectionObserver.prototype._checkForIntersections = function() { 480 | if (!this.root && crossOriginUpdater && !crossOriginRect) { 481 | // Cross origin monitoring, but no initial data available yet. 482 | return; 483 | } 484 | 485 | var rootIsInDom = this._rootIsInDom(); 486 | var rootRect = rootIsInDom ? this._getRootRect() : getEmptyRect(); 487 | 488 | this._observationTargets.forEach(function(item) { 489 | var target = item.element; 490 | var targetRect = getBoundingClientRect(target); 491 | var rootContainsTarget = this._rootContainsTarget(target); 492 | var oldEntry = item.entry; 493 | var intersectionRect = rootIsInDom && rootContainsTarget && 494 | this._computeTargetAndRootIntersection(target, targetRect, rootRect); 495 | 496 | var newEntry = item.entry = new IntersectionObserverEntry({ 497 | time: now(), 498 | target: target, 499 | boundingClientRect: targetRect, 500 | rootBounds: crossOriginUpdater && !this.root ? null : rootRect, 501 | intersectionRect: intersectionRect 502 | }); 503 | 504 | if (!oldEntry) { 505 | this._queuedEntries.push(newEntry); 506 | } else if (rootIsInDom && rootContainsTarget) { 507 | // If the new entry intersection ratio has crossed any of the 508 | // thresholds, add a new entry. 509 | if (this._hasCrossedThreshold(oldEntry, newEntry)) { 510 | this._queuedEntries.push(newEntry); 511 | } 512 | } else { 513 | // If the root is not in the DOM or target is not contained within 514 | // root but the previous entry for this target had an intersection, 515 | // add a new record indicating removal. 516 | if (oldEntry && oldEntry.isIntersecting) { 517 | this._queuedEntries.push(newEntry); 518 | } 519 | } 520 | }, this); 521 | 522 | if (this._queuedEntries.length) { 523 | this._callback(this.takeRecords(), this); 524 | } 525 | }; 526 | 527 | 528 | /** 529 | * Accepts a target and root rect computes the intersection between then 530 | * following the algorithm in the spec. 531 | * TODO(philipwalton): at this time clip-path is not considered. 532 | * https://w3c.github.io/IntersectionObserver/#calculate-intersection-rect-algo 533 | * @param {Element} target The target DOM element 534 | * @param {Object} targetRect The bounding rect of the target. 535 | * @param {Object} rootRect The bounding rect of the root after being 536 | * expanded by the rootMargin value. 537 | * @return {?Object} The final intersection rect object or undefined if no 538 | * intersection is found. 539 | * @private 540 | */ 541 | IntersectionObserver.prototype._computeTargetAndRootIntersection = 542 | function(target, targetRect, rootRect) { 543 | // If the element isn't displayed, an intersection can't happen. 544 | if (window.getComputedStyle(target).display == 'none') return; 545 | 546 | var intersectionRect = targetRect; 547 | var parent = getParentNode(target); 548 | var atRoot = false; 549 | 550 | while (!atRoot && parent) { 551 | var parentRect = null; 552 | var parentComputedStyle = parent.nodeType == 1 ? 553 | window.getComputedStyle(parent) : {}; 554 | 555 | // If the parent isn't displayed, an intersection can't happen. 556 | if (parentComputedStyle.display == 'none') return null; 557 | 558 | if (parent == this.root || parent.nodeType == /* DOCUMENT */ 9) { 559 | atRoot = true; 560 | if (parent == this.root || parent == document) { 561 | if (crossOriginUpdater && !this.root) { 562 | if (!crossOriginRect || 563 | crossOriginRect.width == 0 && crossOriginRect.height == 0) { 564 | // A 0-size cross-origin intersection means no-intersection. 565 | parent = null; 566 | parentRect = null; 567 | intersectionRect = null; 568 | } else { 569 | parentRect = crossOriginRect; 570 | } 571 | } else { 572 | parentRect = rootRect; 573 | } 574 | } else { 575 | // Check if there's a frame that can be navigated to. 576 | var frame = getParentNode(parent); 577 | var frameRect = frame && getBoundingClientRect(frame); 578 | var frameIntersect = 579 | frame && 580 | this._computeTargetAndRootIntersection(frame, frameRect, rootRect); 581 | if (frameRect && frameIntersect) { 582 | parent = frame; 583 | parentRect = convertFromParentRect(frameRect, frameIntersect); 584 | } else { 585 | parent = null; 586 | intersectionRect = null; 587 | } 588 | } 589 | } else { 590 | // If the element has a non-visible overflow, and it's not the 591 | // or element, update the intersection rect. 592 | // Note: and cannot be clipped to a rect that's not also 593 | // the document rect, so no need to compute a new intersection. 594 | var doc = parent.ownerDocument; 595 | if (parent != doc.body && 596 | parent != doc.documentElement && 597 | parentComputedStyle.overflow != 'visible') { 598 | parentRect = getBoundingClientRect(parent); 599 | } 600 | } 601 | 602 | // If either of the above conditionals set a new parentRect, 603 | // calculate new intersection data. 604 | if (parentRect) { 605 | intersectionRect = computeRectIntersection(parentRect, intersectionRect); 606 | } 607 | if (!intersectionRect) break; 608 | parent = parent && getParentNode(parent); 609 | } 610 | return intersectionRect; 611 | }; 612 | 613 | 614 | /** 615 | * Returns the root rect after being expanded by the rootMargin value. 616 | * @return {ClientRect} The expanded root rect. 617 | * @private 618 | */ 619 | IntersectionObserver.prototype._getRootRect = function() { 620 | var rootRect; 621 | if (this.root) { 622 | rootRect = getBoundingClientRect(this.root); 623 | } else { 624 | // Use / instead of window since scroll bars affect size. 625 | var html = document.documentElement; 626 | var body = document.body; 627 | rootRect = { 628 | top: 0, 629 | left: 0, 630 | right: html.clientWidth || body.clientWidth, 631 | width: html.clientWidth || body.clientWidth, 632 | bottom: html.clientHeight || body.clientHeight, 633 | height: html.clientHeight || body.clientHeight 634 | }; 635 | } 636 | return this._expandRectByRootMargin(rootRect); 637 | }; 638 | 639 | 640 | /** 641 | * Accepts a rect and expands it by the rootMargin value. 642 | * @param {DOMRect|ClientRect} rect The rect object to expand. 643 | * @return {ClientRect} The expanded rect. 644 | * @private 645 | */ 646 | IntersectionObserver.prototype._expandRectByRootMargin = function(rect) { 647 | var margins = this._rootMarginValues.map(function(margin, i) { 648 | return margin.unit == 'px' ? margin.value : 649 | margin.value * (i % 2 ? rect.width : rect.height) / 100; 650 | }); 651 | var newRect = { 652 | top: rect.top - margins[0], 653 | right: rect.right + margins[1], 654 | bottom: rect.bottom + margins[2], 655 | left: rect.left - margins[3] 656 | }; 657 | newRect.width = newRect.right - newRect.left; 658 | newRect.height = newRect.bottom - newRect.top; 659 | 660 | return newRect; 661 | }; 662 | 663 | 664 | /** 665 | * Accepts an old and new entry and returns true if at least one of the 666 | * threshold values has been crossed. 667 | * @param {?IntersectionObserverEntry} oldEntry The previous entry for a 668 | * particular target element or null if no previous entry exists. 669 | * @param {IntersectionObserverEntry} newEntry The current entry for a 670 | * particular target element. 671 | * @return {boolean} Returns true if a any threshold has been crossed. 672 | * @private 673 | */ 674 | IntersectionObserver.prototype._hasCrossedThreshold = 675 | function(oldEntry, newEntry) { 676 | 677 | // To make comparing easier, an entry that has a ratio of 0 678 | // but does not actually intersect is given a value of -1 679 | var oldRatio = oldEntry && oldEntry.isIntersecting ? 680 | oldEntry.intersectionRatio || 0 : -1; 681 | var newRatio = newEntry.isIntersecting ? 682 | newEntry.intersectionRatio || 0 : -1; 683 | 684 | // Ignore unchanged ratios 685 | if (oldRatio === newRatio) return; 686 | 687 | for (var i = 0; i < this.thresholds.length; i++) { 688 | var threshold = this.thresholds[i]; 689 | 690 | // Return true if an entry matches a threshold or if the new ratio 691 | // and the old ratio are on the opposite sides of a threshold. 692 | if (threshold == oldRatio || threshold == newRatio || 693 | threshold < oldRatio !== threshold < newRatio) { 694 | return true; 695 | } 696 | } 697 | }; 698 | 699 | 700 | /** 701 | * Returns whether or not the root element is an element and is in the DOM. 702 | * @return {boolean} True if the root element is an element and is in the DOM. 703 | * @private 704 | */ 705 | IntersectionObserver.prototype._rootIsInDom = function() { 706 | return !this.root || containsDeep(document, this.root); 707 | }; 708 | 709 | 710 | /** 711 | * Returns whether or not the target element is a child of root. 712 | * @param {Element} target The target element to check. 713 | * @return {boolean} True if the target element is a child of root. 714 | * @private 715 | */ 716 | IntersectionObserver.prototype._rootContainsTarget = function(target) { 717 | return containsDeep(this.root || document, target) && 718 | (!this.root || this.root.ownerDocument == target.ownerDocument); 719 | }; 720 | 721 | 722 | /** 723 | * Adds the instance to the global IntersectionObserver registry if it isn't 724 | * already present. 725 | * @private 726 | */ 727 | IntersectionObserver.prototype._registerInstance = function() { 728 | if (registry.indexOf(this) < 0) { 729 | registry.push(this); 730 | } 731 | }; 732 | 733 | 734 | /** 735 | * Removes the instance from the global IntersectionObserver registry. 736 | * @private 737 | */ 738 | IntersectionObserver.prototype._unregisterInstance = function() { 739 | var index = registry.indexOf(this); 740 | if (index != -1) registry.splice(index, 1); 741 | }; 742 | 743 | 744 | /** 745 | * Returns the result of the performance.now() method or null in browsers 746 | * that don't support the API. 747 | * @return {number} The elapsed time since the page was requested. 748 | */ 749 | function now() { 750 | return window.performance && performance.now && performance.now(); 751 | } 752 | 753 | 754 | /** 755 | * Throttles a function and delays its execution, so it's only called at most 756 | * once within a given time period. 757 | * @param {Function} fn The function to throttle. 758 | * @param {number} timeout The amount of time that must pass before the 759 | * function can be called again. 760 | * @return {Function} The throttled function. 761 | */ 762 | function throttle(fn, timeout) { 763 | var timer = null; 764 | return function () { 765 | if (!timer) { 766 | timer = setTimeout(function() { 767 | fn(); 768 | timer = null; 769 | }, timeout); 770 | } 771 | }; 772 | } 773 | 774 | 775 | /** 776 | * Adds an event handler to a DOM node ensuring cross-browser compatibility. 777 | * @param {Node} node The DOM node to add the event handler to. 778 | * @param {string} event The event name. 779 | * @param {Function} fn The event handler to add. 780 | * @param {boolean} opt_useCapture Optionally adds the even to the capture 781 | * phase. Note: this only works in modern browsers. 782 | */ 783 | function addEvent(node, event, fn, opt_useCapture) { 784 | if (typeof node.addEventListener == 'function') { 785 | node.addEventListener(event, fn, opt_useCapture || false); 786 | } 787 | else if (typeof node.attachEvent == 'function') { 788 | node.attachEvent('on' + event, fn); 789 | } 790 | } 791 | 792 | 793 | /** 794 | * Removes a previously added event handler from a DOM node. 795 | * @param {Node} node The DOM node to remove the event handler from. 796 | * @param {string} event The event name. 797 | * @param {Function} fn The event handler to remove. 798 | * @param {boolean} opt_useCapture If the event handler was added with this 799 | * flag set to true, it should be set to true here in order to remove it. 800 | */ 801 | function removeEvent(node, event, fn, opt_useCapture) { 802 | if (typeof node.removeEventListener == 'function') { 803 | node.removeEventListener(event, fn, opt_useCapture || false); 804 | } 805 | else if (typeof node.detatchEvent == 'function') { 806 | node.detatchEvent('on' + event, fn); 807 | } 808 | } 809 | 810 | 811 | /** 812 | * Returns the intersection between two rect objects. 813 | * @param {Object} rect1 The first rect. 814 | * @param {Object} rect2 The second rect. 815 | * @return {?Object|?ClientRect} The intersection rect or undefined if no 816 | * intersection is found. 817 | */ 818 | function computeRectIntersection(rect1, rect2) { 819 | var top = Math.max(rect1.top, rect2.top); 820 | var bottom = Math.min(rect1.bottom, rect2.bottom); 821 | var left = Math.max(rect1.left, rect2.left); 822 | var right = Math.min(rect1.right, rect2.right); 823 | var width = right - left; 824 | var height = bottom - top; 825 | 826 | return (width >= 0 && height >= 0) && { 827 | top: top, 828 | bottom: bottom, 829 | left: left, 830 | right: right, 831 | width: width, 832 | height: height 833 | } || null; 834 | } 835 | 836 | 837 | /** 838 | * Shims the native getBoundingClientRect for compatibility with older IE. 839 | * @param {Element} el The element whose bounding rect to get. 840 | * @return {DOMRect|ClientRect} The (possibly shimmed) rect of the element. 841 | */ 842 | function getBoundingClientRect(el) { 843 | var rect; 844 | 845 | try { 846 | rect = el.getBoundingClientRect(); 847 | } catch (err) { 848 | // Ignore Windows 7 IE11 "Unspecified error" 849 | // https://github.com/w3c/IntersectionObserver/pull/205 850 | } 851 | 852 | if (!rect) return getEmptyRect(); 853 | 854 | // Older IE 855 | if (!(rect.width && rect.height)) { 856 | rect = { 857 | top: rect.top, 858 | right: rect.right, 859 | bottom: rect.bottom, 860 | left: rect.left, 861 | width: rect.right - rect.left, 862 | height: rect.bottom - rect.top 863 | }; 864 | } 865 | return rect; 866 | } 867 | 868 | 869 | /** 870 | * Returns an empty rect object. An empty rect is returned when an element 871 | * is not in the DOM. 872 | * @return {ClientRect} The empty rect. 873 | */ 874 | function getEmptyRect() { 875 | return { 876 | top: 0, 877 | bottom: 0, 878 | left: 0, 879 | right: 0, 880 | width: 0, 881 | height: 0 882 | }; 883 | } 884 | 885 | 886 | /** 887 | * Ensure that the result has all of the necessary fields of the DOMRect. 888 | * Specifically this ensures that `x` and `y` fields are set. 889 | * 890 | * @param {?DOMRect|?ClientRect} rect 891 | * @return {?DOMRect} 892 | */ 893 | function ensureDOMRect(rect) { 894 | // A `DOMRect` object has `x` and `y` fields. 895 | if (!rect || 'x' in rect) { 896 | return rect; 897 | } 898 | // A IE's `ClientRect` type does not have `x` and `y`. The same is the case 899 | // for internally calculated Rect objects. For the purposes of 900 | // `IntersectionObserver`, it's sufficient to simply mirror `left` and `top` 901 | // for these fields. 902 | return { 903 | top: rect.top, 904 | y: rect.top, 905 | bottom: rect.bottom, 906 | left: rect.left, 907 | x: rect.left, 908 | right: rect.right, 909 | width: rect.width, 910 | height: rect.height 911 | }; 912 | } 913 | 914 | 915 | /** 916 | * Inverts the intersection and bounding rect from the parent (frame) BCR to 917 | * the local BCR space. 918 | * @param {DOMRect|ClientRect} parentBoundingRect The parent's bound client rect. 919 | * @param {DOMRect|ClientRect} parentIntersectionRect The parent's own intersection rect. 920 | * @return {ClientRect} The local root bounding rect for the parent's children. 921 | */ 922 | function convertFromParentRect(parentBoundingRect, parentIntersectionRect) { 923 | var top = parentIntersectionRect.top - parentBoundingRect.top; 924 | var left = parentIntersectionRect.left - parentBoundingRect.left; 925 | return { 926 | top: top, 927 | left: left, 928 | height: parentIntersectionRect.height, 929 | width: parentIntersectionRect.width, 930 | bottom: top + parentIntersectionRect.height, 931 | right: left + parentIntersectionRect.width 932 | }; 933 | } 934 | 935 | 936 | /** 937 | * Checks to see if a parent element contains a child element (including inside 938 | * shadow DOM). 939 | * @param {Node} parent The parent element. 940 | * @param {Node} child The child element. 941 | * @return {boolean} True if the parent node contains the child node. 942 | */ 943 | function containsDeep(parent, child) { 944 | var node = child; 945 | while (node) { 946 | if (node == parent) return true; 947 | 948 | node = getParentNode(node); 949 | } 950 | return false; 951 | } 952 | 953 | 954 | /** 955 | * Gets the parent node of an element or its host element if the parent node 956 | * is a shadow root. 957 | * @param {Node} node The node whose parent to get. 958 | * @return {Node|null} The parent node or null if no parent exists. 959 | */ 960 | function getParentNode(node) { 961 | var parent = node.parentNode; 962 | 963 | if (node.nodeType == /* DOCUMENT */ 9 && node != document) { 964 | // If this node is a document node, look for the embedding frame. 965 | return getFrameElement(node); 966 | } 967 | 968 | if (parent && parent.nodeType == 11 && parent.host) { 969 | // If the parent is a shadow root, return the host element. 970 | return parent.host; 971 | } 972 | 973 | if (parent && parent.assignedSlot) { 974 | // If the parent is distributed in a , return the parent of a slot. 975 | return parent.assignedSlot.parentNode; 976 | } 977 | 978 | return parent; 979 | } 980 | 981 | 982 | // Exposes the constructors globally. 983 | window.IntersectionObserver = IntersectionObserver; 984 | window.IntersectionObserverEntry = IntersectionObserverEntry; 985 | 986 | }()); 987 | --------------------------------------------------------------------------------