├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── build ├── rollup.conf.banner.js ├── rollup.conf.js └── rollup.conf.min.js ├── dist ├── scrollreveal.es.js ├── scrollreveal.js └── scrollreveal.min.js ├── package.json ├── src ├── index.js ├── instance │ ├── constructor.js │ ├── defaults.js │ ├── functions │ │ ├── animate.js │ │ ├── delegate.js │ │ ├── initialize.js │ │ ├── rinse.js │ │ ├── sequence.js │ │ └── style.js │ ├── methods │ │ ├── clean.js │ │ ├── destroy.js │ │ ├── reveal.js │ │ └── sync.js │ └── mount.js ├── polyfills │ └── math-sign.js └── utils │ ├── deep-assign.js │ ├── each.js │ ├── get-geometry.js │ ├── get-prefixed-css-prop.js │ ├── get-scrolled.js │ ├── is-element-visible.js │ ├── is-mobile.js │ ├── is-object.js │ ├── is-transform-supported.js │ ├── is-transition-supported.js │ ├── logger.js │ └── next-unique-id.js └── test ├── instance └── constructor.spec.js ├── karma.conf.js ├── polyfills └── math-sign.spec.js ├── sauce.conf.js ├── timeout.spec.js └── utils ├── deep-assign.spec.js ├── each.spec.js ├── get-prefixed-css-prop.spec.js ├── is-mobile.spec.js ├── is-object.spec.js ├── is-transform-supported.spec.js ├── is-transition-supported.spec.js ├── logger.spec.js └── next-unique-id.spec.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "amd": true, 5 | "browser": true, 6 | "mocha": true, 7 | "node": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "parserOptions": { 11 | "sourceType": "module" 12 | }, 13 | "globals": { 14 | "describe": true, 15 | "it": true, 16 | "expect": true, 17 | "sinon": true 18 | }, 19 | "rules": { 20 | "no-cond-assign": 2, 21 | "no-console": 1, 22 | "no-const-assign": 2, 23 | "no-class-assign": 2, 24 | "no-this-before-super": 2, 25 | "no-unused-vars": 1, 26 | "no-var": 2, 27 | "object-shorthand": [2, "always"] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | <!-- 2 | 3 | Thanks for raising an issue! To help us help you, if you've found a bug please consider the following: 4 | 5 | * If you can demonstrate the bug using JSBin: https://goo.gl/6b4OeX — please do. 6 | * If that's not possible, perhaps because your bug involves plugins, we recommend creating a small repo that illustrates the problem. 7 | 8 | And please, search existing issues before creating a new one. 9 | 10 | --> 11 | 12 | ### Environment 13 | 14 | * Operating System: 15 | * Browser Version: 16 | * ScrollReveal Version: 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | <!-- 2 | 3 | Thank you for creating a pull request. Before submitting, please note the following: 4 | 5 | * If your pull request implements a new feature, please raise an issue to discuss it before sending code. 6 | * This message body should clearly illustrate what problems it solves. 7 | * If there are related issues, remember to reference them. 8 | * Ideally, include a test that fails without this PR but passes with it. PRs will only be merged once they pass CI. 9 | 10 | --> 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .ignore/ 3 | .vscode/ 4 | node_modules/ 5 | yarn.lock 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: trusty 3 | node_js: 4 | - '9' 5 | addons: 6 | chrome: stable 7 | hosts: localsauce 8 | sudo: required 9 | before_script: 10 | - 'sudo chown root /opt/google/chrome/chrome-sandbox' 11 | - 'sudo chmod 4755 /opt/google/chrome/chrome-sandbox' 12 | after_success: 13 | - npm run coverage 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [4.0.9] - 2021-03-04 4 | 5 | ### Fixed 6 | 7 | - Styles applied using CSSOM don't drop `:` characters. 8 | 9 | ## [4.0.8] - 2021-03-02 10 | 11 | ### Fixed 12 | 13 | - Avoid Content Security Policy (CSP) violations. [@lambdacasserole](https://github.com/lambdacasserole) [#431](https://github.com/jlmakes/scrollreveal/pull/431) 14 | 15 | ## [4.0.7] - 2020-07-15 16 | 17 | ### Fixed 18 | 19 | - Ensure element geometry exists. [#437](https://github.com/jlmakes/scrollreveal/issues/437) 20 | 21 | ## [4.0.6] - 2020-03-15 22 | 23 | ### Fixed 24 | 25 | - Default transition values of `none` are now correctly ignored. [#231](https://github.com/jlmakes/scrollreveal/issues/231) 26 | 27 | ### Fixed 28 | 29 | ## [4.0.5] - 2018-10-20 30 | 31 | ### Fixed 32 | 33 | - Calling `reveal()` on the same `target` breaking animation. [#468](https://github.com/jlmakes/scrollreveal/issues/468) 34 | 35 | ## [4.0.4] - 2018-09-22 36 | 37 | ### Fixed 38 | 39 | - Malformed `package.json` 40 | 41 | ## [4.0.3] - 2018-09-21 42 | 43 | ### Fixed 44 | 45 | – `options.cleanup` is now correctly set to `false` by default. [#457](https://github.com/jlmakes/scrollreveal/issues/457) 46 | 47 | ## [4.0.2] - 2018-09-11 48 | 49 | ### Fixed 50 | 51 | - Null property assignment regression in mount function. [#456](https://github.com/jlmakes/scrollreveal/issues/456) 52 | 53 | ## [4.0.1] - 2018-09-09 54 | 55 | ### Fixed 56 | 57 | - Noop instances were not correctly unmounting from the DOM. [#455](https://github.com/jlmakes/scrollreveal/issues/455) 58 | - Readme links to pricing page no longer 404. 59 | 60 | ## [4.0.0] - 2018-08-06 61 | 62 | ### Added 63 | 64 | - ScrollReveal can be enabled/disabled on desktops using `options.desktop`. 65 | - The class `sr` is added to `<html>` during instantiation when supported. [#294](https://github.com/jlmakes/scrollreveal/issues/294) 66 | - `height: 100%` is added to `<body>` during instantiation when supported. [#298](https://github.com/jlmakes/scrollreveal/issues/298) 67 | - Unused containers are removed from the store, and their event listeners destroyed. 68 | - ScrollReveal skips generating opacity styles when `options.opacity` is set to `null`. 69 | - ScrollReveal retains element CSS transformations. [#251](https://github.comjlmakes/scrollreveal/issues/251) 70 | - New `options.cleanup` toggles whether generated styles are removed upon reveal completion (when `options.reset` is `false`). [#292](https://github.comjlmakes/scrollreveal/issues/292) 71 | - ScrollReveal tracks scroll direction as container store data. [#384](https://github.com/jlmakes/scrollreveal/issues/384) 72 | - New `clean()` method removes specific generated styles and event listeners. [#227](https://github.com/jlmakes/scrollreveal/issues/227) 73 | - New `destroy()` method removes all generated styles and event listeners. [#227](https://github.com/jlmakes/scrollreveal/issues/227) 74 | - New `debug` static property toggles error messages in console. [#351](https://github.com/jlmakes/scrollreveal/issues/351) 75 | - Instance methods now accept native arrays of HTML elements. 76 | 77 | ### Changed 78 | 79 | - **Breaking:** The `reveal()` method no longer accepts an `interval` parameter. Instead, sequence intervals are now defined with `options.interval`. 80 | - **Breaking:** The instance method `isSupported()` is now static. 81 | - **Breaking:** `options.distance` supports only `em` `px` and `%` values. 82 | - **Breaking:** ScrollReveal methods are no longer chainable. 83 | - **Breaking:** ScrollReveal requires a commercial license, unless for [GPL-3.0](https://opensource.org/licenses/GPL-3.0) compatible open source projects. 84 | - Elements in a reveal sequence are no longer grouped, and reveal progressively when visible. 85 | - ScrollReveal uses a single `matrix3d()` property, with the correct prefix and only when necessary. [#292](https://github.com/jlmakes/scrollreveal/issues/292) 86 | - ScrollReveal returns a non-operational instance when instantiated in unsupported browsers. 87 | - ScrollReveal `version` is now a read-only instance property. 88 | - ScrollReveal methods are now bound read-only instance properties. 89 | - `options.viewFactor` clamps values outside of `0.0` to `1.0`. 90 | - ScrollReveal constructor now returns a singleton. 91 | 92 | ### Fixed 93 | 94 | - The `requestAnimationFrame` polyfill now reliably throttles callback invocations. 95 | 96 | ## [3.3.6] - 2017-06-23 97 | 98 | ### Fixed 99 | 100 | - Element visibility now checks left and right boundaries correctly. [#352](https://github.com/jlmakes/scrollreveal/issues/352) 101 | - Library version instance property is again accurate. 102 | 103 | ## [3.3.5] - 2017-04-05 104 | 105 | ### Fixed 106 | 107 | - Patched to ensure version 3 is the default NPM package. 108 | 109 | ## [3.3.4] - 2017-02-18 110 | 111 | ### Fixed 112 | 113 | - Update stale CDN link in README. 114 | 115 | ### Changed 116 | 117 | - Add deprecation warnings to README. 118 | 119 | ## [3.3.3] - 2017-02-18 120 | 121 | ### Fixed 122 | 123 | - Fix error when using Bower and Wordpress due to missing semi-colon. [#278](https://github.com/jlmakes/scrollreveal/issues/278) 124 | 125 | ## [3.3.2] - 2016-10-02 126 | 127 | ### Changed 128 | 129 | - Updated Starting Defaults section in README. [#273](https://github.comjlmakes/scrollreveal/issues/273) 130 | 131 | ### Fixed 132 | 133 | - Using a selector to define a default container during instantiation now works. [#289](https://github.com/jlmakes/scrollreveal/issues/289) 134 | 135 | ## [3.3.1] - 2016-07-22 136 | 137 | ### Fixed 138 | 139 | - Instance variable `version` updated with correct library version. 140 | 141 | ## [3.3.0] - 2016-07-22 142 | 143 | ### Added 144 | 145 | - New callback `beforeReveal(el)`. [#273](https://github.comjlmakes/scrollreveal/issues/273) 146 | - New callback `beforeReset(el)`. [#273](https://github.com/jlmakes/scrollreveal/issues/273) 147 | 148 | ## [3.2.0] - 2016-07-08 149 | 150 | ### Added 151 | 152 | - New `isNodeList()` method added to `Tools`. 153 | - New `version` instance variable contains library version. 154 | - HTML Collections are now supported as the first argument in `reveal()`. [#246](https://github.com/jlmakes/scrollreveal/issues/246) 155 | - Added fallback for `requestAnimationFrame`. [#267](https://github.comjlmakes/scrollreveal/issues/267) 156 | 157 | ### Changed 158 | 159 | - Updated Starting Defaults section in README. 160 | 161 | ### Fixed 162 | 163 | - Calling `reveal()` multiple times on an element with `config.origin` as `top` or `left` no longer produces invalid CSS. [#270](https://github.com/jlmakes/scrollreveal/issues/270) 164 | - Refactored AMD/CommonJS module wrapper to work with Codekit. [#253](https://github.com/jlmakes/scrollreveal/issues/253) 165 | 166 | ## [3.1.5] - 2016-07-06 167 | 168 | ### Fixed 169 | 170 | - `sync()` method now properly supports sequences. 171 | 172 | ## [3.1.4] - 2016-03-28 173 | 174 | ### Changed 175 | 176 | - Added `console.log` calls back to non-minified distribution. [#235](https://github.com/jlmakes/scrollreveal/issues/235) 177 | 178 | ## [3.1.3] - 2016-03-28 179 | 180 | ### Removed 181 | 182 | - Removed `console.log` calls from distribution. [#235](https://github.comjlmakes/scrollreveal/issues/235) 183 | 184 | ## [3.1.2] - 2016-03-23 185 | 186 | ### Fixed 187 | 188 | - Removed stray quotation mark in `reveal()` error message. 189 | 190 | ## [3.1.1] - 2016-03-08 191 | 192 | ### Fixed 193 | 194 | - `config.reset` now works properly with sequences. [#241](https://github.comjlmakes/scrollreveal/issues/241) 195 | 196 | ## [3.1.0] - 2016-03-07 197 | 198 | ### Added 199 | 200 | - New `isNode()` method added to `Tools`. 201 | - HTML elements are now supported as the first argument in `reveal()`. 202 | - Selector strings assigned to `config.container` are now supported. 203 | - `reveal()` now accepts an `interval` as it's last argument to create sequences. [#86](https://github.com/jlmakes/scrollreveal/issues/86) [#180](https://github.com/jlmakes/scrollreveal/issues/180) [#187](https://github.comjlmakes/scrollreveal/issues/187) [#215](https://github.com/jlmakes/scrollreveal/issues/215) [#234](https://github.com/jlmakes/scrollreveal/issues/234) 204 | - New section on sequenced animations added to README. 205 | 206 | ### Changed 207 | 208 | - Messages logged to console are now prepended with `ScrollReveal:` for clarity. 209 | - Revised and renamed `supported()` method to `isSupported()`. 210 | - Updated Custom Containers section in README with an example using a selector. 211 | - Updated Tips section in README. 212 | 213 | ### Fixed 214 | 215 | - Added semi-colon before global IIFE to improve reliability. [#228](https://github.com/jlmakes/scrollreveal/issues/228) 216 | - The existence of `console.log` is now confirmed for IE9. [#230](https://github.com/jlmakes/scrollreveal/issues/230) 217 | - Typos, indentation and semicolons corrected in README. 218 | 219 | ## [3.0.9] - 2016-01-14 220 | 221 | ### Changed 222 | 223 | - Updated example site links in the README. 224 | 225 | ### Fixed 226 | 227 | - Fixed operator mismatch inside `supported()`. [#220](https://github.comjlmakes/scrollreveal/issues/220) 228 | 229 | ## [3.0.8] - 2016-01-13 230 | 231 | ### Changed 232 | 233 | - Public methods now verify that ScrollReveal is supported. 234 | 235 | ### Fixed 236 | 237 | - Updated Tips section in README. 238 | 239 | ## [3.0.7] - 2016-01-13 240 | 241 | ### Added 242 | 243 | - Added brower support information to README. [#219](https://github.comjlmakes/scrollreveal/issues/219) 244 | 245 | ### Changed 246 | 247 | - `console.log` is now used instead of `console.warn`. [#215](https://github.com/jlmakes/scrollreveal/issues/215) 248 | - Moved `tools.isSupported` method to `ScrollReveal.prototype.supported`. 249 | - Updated the configuration and tips documentation in the README. 250 | 251 | ### Removed 252 | 253 | - The `init()` method was removed. 254 | 255 | ### Fixed 256 | 257 | - Using `config.mobile` in `reveal()` now works. [#216](https://github.comjlmakes/scrollreveal/issues/216) 258 | 259 | ## [3.0.6] - 2016-01-02 260 | 261 | ### Fixed 262 | 263 | - Custom default containers are now used. 264 | - Critical issues affecting Chrome on iOS were (finally) solved. [#196](https://github.com/jlmakes/scrollreveal/issues/196) 265 | - Revisited `3.0.4` changes to chaining `reveal()` calls. [#212](https://github.com/jlmakes/scrollreveal/issues/212) 266 | 267 | ## [3.0.5] - 2015-12-30 268 | 269 | ### Fixed 270 | 271 | - Fixed compatibility issues with Webpack. [#209](https://github.comjlmakes/scrollreveal/issues/209) 272 | 273 | ## [3.0.4] - 2015-12-30 274 | 275 | ### Fixed 276 | 277 | - Squashed Webkit browser bugs due to syntax errors. [#208](https://github.comjlmakes/scrollreveal/issues/208) 278 | - Chaining `reveal()` calls no longer prematurely initialize animation. 279 | - Cleaned up README typos, and stale reference to `config.wait`. 280 | 281 | ## [3.0.3] - 2015-12-22 282 | 283 | ### Changed 284 | 285 | - `reveal()` and `sync()` now return the ScrollReveal instance even on failure. [#198](https://github.com/jlmakes/scrollreveal/issues/198) 286 | 287 | ## [3.0.2] - 2015-12-22 288 | 289 | ### Added 290 | 291 | - Added `bower.json` to release package. [#199](https://github.comjlmakes/scrollreveal/issues/199) 292 | 293 | ### Fixed 294 | 295 | - Preexisting CSS transition styles are no longer destroyed. [#197](https://github.com/jlmakes/scrollreveal/issues/197) 296 | 297 | ## [3.0.1] - 2015-12-21 298 | 299 | ### Changed 300 | 301 | - Updated Getting Started section in the README. 302 | 303 | ### Fixed 304 | 305 | - Hard learned NPM and Bower issues related to release management were endured. 306 | - Issues related to element visibility and animation behavior were addressed. [#193](https://github.com/jlmakes/scrollreveal/issues/193) [#196](https://github.comjlmakes/scrollreveal/issues/196) 307 | 308 | ## [3.0.0] - 2015-12-15 309 | 310 | This version marks a significant change in how developers use ScrollReveal, introducing a JavaScript API to replace the inline attribute parser. It's a big shift, but prioritizes maintainability and flexibility over the novelty of natural language parsing. 311 | 312 | ### Added 313 | 314 | - New method `reveal()`. [#1](https://github.com/jlmakes/scrollreveal/issues/1) [#122](https://github.com/jlmakes/scrollreveal/issues/122) 315 | - New method `sync()`. 316 | - New callback `config.afterReset`. 317 | - Horizontal scrolling is now supported. [#184](https://github.comjlmakes/scrollreveal/issues/184) 318 | 319 | ### Changed 320 | 321 | - **Breaking:** `config.enter` renamed `config.origin`. 322 | - **Breaking:** `config.wait` renamed `config.delay`. 323 | - **Breaking:** `config.delay` renamed `config.useDelay`. 324 | - **Breaking:** `config.over` renamed `config.duration`. 325 | - **Breaking:** `config.move` renamed `config.distance`. 326 | - **Breaking:** `config.viewport` renamed `config.container`. 327 | - **Breaking:** `config.vFactor` renamed `config.viewFactor`. 328 | - **Breaking:** `config.complete` renamed `config.afterReveal`. 329 | - **Breaking:** Time values are now expected in milliseconds (instead of `string`). 330 | - **Breaking:** `config.scale` expects value type `number` (instead of `object`). 331 | - **Breaking:** `config.rotation` axis values require `string` with unit type (instead of `number`). 332 | - **Breaking:** ScrollReveal constructor is now capitalized. 333 | - Reveals now resolve to element's computed opacity, instead of `1`. [#185](https://github.com/jlmakes/scrollreveal/issues/185) 334 | 335 | ### Removed 336 | 337 | - ScrollReveal no longer recognizes `data-sr` attributes. 338 | 339 | ### Fixed 340 | 341 | - Improved reliability of callback timers. 342 | 343 | ## [2.3.2] - 2015-06-15 344 | 345 | ### Changed 346 | 347 | - Updated `bower.json` syntax. [#150](https://github.com/jlmakes/scrollreveal/issues/150) 348 | 349 | ## [2.3.1] - 2015-06-04 350 | 351 | ### Added 352 | 353 | - Simple instantiation (without `new` keyword) is now supported. [#148](https://github.com/jlmakes/scrollreveal/issues/148) 354 | 355 | ## [2.3.0] - 2015-04-25 356 | 357 | ### Added 358 | 359 | - New keyword `vFactor` and alias `vF` control when an element is considered visible. 360 | - New keyword `opacity` controls starting opacity. 361 | 362 | ### Removed 363 | 364 | - The easing keyword `hustle` was removed. 365 | 366 | ## [2.2.0] - 2015-03-18 367 | 368 | ### Added 369 | 370 | - New keyword `spin` controls yaw. 371 | - New keyword `roll` controls roll. 372 | - New keyword `flip` controls pitch. 373 | 374 | ### Changed 375 | 376 | - Improved Basic Usage examples in README. 377 | 378 | ## [2.1.0] - 2014-11-25 379 | 380 | ### Added 381 | 382 | - Various tablets added to mobile device detection. [#32](https://github.comjlmakes/scrollreveal/issues/32) [#81](https://github.com/jlmakes/scrollreveal/issues/81) 383 | - CSS Transition support is now confirmed during instantiation. [#109](https://github.com/jlmakes/scrollreveal/issues/109) 384 | 385 | ## [2.0.5] - 2014-11-23 386 | 387 | ### Changed 388 | 389 | - Reverted `2.0.4` change to element animation logic. [#108](https://github.comjlmakes/scrollreveal/issues/108) 390 | 391 | ## [2.0.4] - 2014-11-21 392 | 393 | ### Changed 394 | 395 | - Revised how element animations are handled. 396 | - Reverted `2.0.3` change to element visibility logic. [#106](https://github.com/jlmakes/scrollreveal/issues/106) 397 | 398 | ## [2.0.3] - 2014-11-14 399 | 400 | ### Added 401 | 402 | - `data-sr` attributes are now stripped from initialized elements. [#100](https://github.com/jlmakes/scrollreveal/issues/100) @orapouso. 403 | - Live Reload added to development environment. 404 | 405 | ### Changed 406 | 407 | - Revised how element visibility is determined. 408 | 409 | ### Removed 410 | 411 | - Multiple instances sharing the same viewport element no longer throw an error. [#98](https://github.com/jlmakes/scrollreveal/issues/98) @orapouso. 412 | 413 | ### Fixed 414 | 415 | - Incomplete support for `config.delay = "onload"` was addressed. 416 | - Issues related to `setTimeout`, `config.complete` and incorrect animation timing were addressed. [#96](https://github.com/jlmakes/scrollreveal/issues/96) 417 | 418 | ## [2.0.2] - 2014-10-23 419 | 420 | ### Added 421 | 422 | - An error is now thrown when multiple instances share the same viewport element. [#91](https://github.com/jlmakes/scrollreveal/issues/91) 423 | 424 | ### Fixed 425 | 426 | - Updated NPM and Bower references with new distribution path. 427 | 428 | ## [2.0.1] - 2014-10-18 429 | 430 | ### Fixed 431 | 432 | - Incomplete support for `config.viewport` was addressed. [#67](https://github.com/jlmakes/scrollreveal/issues/67) [#68](https://github.comjlmakes/scrollreveal/issues/68) 433 | 434 | ## [2.0.0] - 2014-10-17 435 | 436 | ### Added 437 | 438 | - New keyword `scale` controls element starting size. 439 | - New option `config.complete` defines a callback for when reveals finish. 440 | - New option `config.viewport` defines custom viewports. 441 | - New option `config.mobile` enables/disables ScrollReveal on mobile devices. 442 | - New option `config.delay` controls when animations are delayed. 443 | 444 | ### Changed 445 | 446 | - **BREAKING:** ScrollReveal now uses the `data-sr` instead of `data-scroll-reveal`. 447 | - Repository now follows [Semantic Versioning](http://semver.org/). 448 | 449 | ### Removed 450 | 451 | - The `after` keyword was removed. 452 | 453 | ## 0.1.3 - 2014-05-26 [YANKED] 454 | 455 | ### Added 456 | 457 | - Configuration now includes starting opacity. [#33](https://github.comjlmakes/scrollreveal/issues/33) @kierzniak 458 | - New `data-scroll-reveal-id` attribute added to revealed DOM elements. 459 | 460 | ### Changed 461 | 462 | - Scroll event handling now uses `requestAnimationFrame`. [#48](https://github.com/jlmakes/scrollreveal/issues/48) @pazguille 463 | - Generated styles are now stored in an object corresponding to the `data-scroll-reveal-id` attribute on each element. [#38](https://github.com/jlmakes/scrollreveal/pull/38) @georgelee1 464 | 465 | ## 0.1.2 - 2014-03-13 [YANKED] 466 | 467 | ### Added 468 | 469 | - Elements with `position: fixed` are now supported. [#35](https://github.comjlmakes/scrollreveal/issues/35) 470 | 471 | ### Fixed 472 | 473 | - Generated styles are now more specific. [#37](https://github.comjlmakes/scrollreveal/issues/37) 474 | 475 | ## 0.1.1 - 2014-03-06 [YANKED] 476 | 477 | ### Fixed 478 | 479 | - Squashed bug with `enter top` and `enter left`. [#13](https://github.comjlmakes/scrollreveal/issues/13) [#31](https://github.com/jlmakes/scrollreveal/issues/31) @sherban @danycerone 480 | 481 | ## 0.1.0 - 2014-03-05 [YANKED] 482 | 483 | ### Added 484 | 485 | - Distribution now supports AMD/CommonJS. 486 | - Repository now uses Gulp. 487 | - Boilerplate Testline suite added to repository. 488 | 489 | ### Changed 490 | 491 | - **BREAKING:** ScrollReveal now uses the `data-scroll-reveal` attribute to parse animation instructions, in place of `data-scrollReveal`. 492 | 493 | ## 0.0.4 - 2014-02-28 [YANKED] 494 | 495 | ### Fixed 496 | 497 | - ScrollReveal no longer destroys the existing style attribute on revealed elements, but instead, now appends the necessary animation styles to existing inline styles. 498 | 499 | ## 0.0.3 - 2014-02-22 [YANKED] 500 | 501 | ### Fixed 502 | 503 | - Removed unused CSS Transition/Transform prefixes for Mozilla and Opera. 504 | 505 | ## 0.0.2 - 2014-02-13 [YANKED] 506 | 507 | ### Added 508 | 509 | - Constructor now accepts a configuration object to customize defaults. 510 | - New `reset` keyword allows elements to reveal each time they enter the viewport. 511 | - The `move` keyword can now be replaced with with CSS easing keywords (e.g. `ease-in-out`). 512 | - Library documentation and code examples added to README. 513 | 514 | ### Changed 515 | 516 | - ScrollReveal is no longer automatically instantiated by the `DOMContentLoaded` event. 517 | 518 | ## 0.0.1 - 2014-01-22 [YANKED] 519 | 520 | ### Hello World 521 | 522 | [4.0.9]: https://github.com/jlmakes/scrollreveal/compare/v4.0.8...v4.0.9 523 | [4.0.8]: https://github.com/jlmakes/scrollreveal/compare/v4.0.7...v4.0.8 524 | [4.0.7]: https://github.com/jlmakes/scrollreveal/compare/v4.0.6...v4.0.7 525 | [4.0.6]: https://github.com/jlmakes/scrollreveal/compare/v4.0.5...v4.0.6 526 | [4.0.5]: https://github.com/jlmakes/scrollreveal/compare/v4.0.4...v4.0.5 527 | [4.0.4]: https://github.com/jlmakes/scrollreveal/compare/v4.0.3...v4.0.4 528 | [4.0.3]: https://github.com/jlmakes/scrollreveal/compare/v4.0.2...v4.0.3 529 | [4.0.2]: https://github.com/jlmakes/scrollreveal/compare/v4.0.1...v4.0.2 530 | [4.0.1]: https://github.com/jlmakes/scrollreveal/compare/v4.0.0...v4.0.1 531 | [4.0.0]: https://github.com/jlmakes/scrollreveal/compare/v3.3.6...v4.0.0 532 | [3.3.6]: https://github.com/jlmakes/scrollreveal/compare/v3.3.5...v3.3.6 533 | [3.3.5]: https://github.com/jlmakes/scrollreveal/compare/v3.3.4...v3.3.5 534 | [3.3.4]: https://github.com/jlmakes/scrollreveal/compare/v3.3.3...v3.3.4 535 | [3.3.3]: https://github.com/jlmakes/scrollreveal/compare/v3.2.2...v3.3.3 536 | [3.3.2]: https://github.com/jlmakes/scrollreveal/compare/v3.3.1...v3.3.2 537 | [3.3.1]: https://github.com/jlmakes/scrollreveal/compare/v3.3.0...v3.3.1 538 | [3.3.0]: https://github.com/jlmakes/scrollreveal/compare/v3.2.0...v3.3.0 539 | [3.2.0]: https://github.com/jlmakes/scrollreveal/compare/v3.1.5...v3.2.0 540 | [3.1.5]: https://github.com/jlmakes/scrollreveal/compare/v3.1.4...v3.1.5 541 | [3.1.4]: https://github.com/jlmakes/scrollreveal/compare/v3.1.3...v3.1.4 542 | [3.1.3]: https://github.com/jlmakes/scrollreveal/compare/v3.1.2...v3.1.3 543 | [3.1.2]: https://github.com/jlmakes/scrollreveal/compare/v3.1.1...v3.1.2 544 | [3.1.1]: https://github.com/jlmakes/scrollreveal/compare/v3.1.0...v3.1.1 545 | [3.1.0]: https://github.com/jlmakes/scrollreveal/compare/v3.0.9...v3.1.0 546 | [3.0.9]: https://github.com/jlmakes/scrollreveal/compare/v3.0.8...v3.0.9 547 | [3.0.8]: https://github.com/jlmakes/scrollreveal/compare/v3.0.7...v3.0.8 548 | [3.0.7]: https://github.com/jlmakes/scrollreveal/compare/v3.0.6...v3.0.7 549 | [3.0.6]: https://github.com/jlmakes/scrollreveal/compare/v3.0.5...v3.0.6 550 | [3.0.5]: https://github.com/jlmakes/scrollreveal/compare/v3.0.4...v3.0.5 551 | [3.0.4]: https://github.com/jlmakes/scrollreveal/compare/v3.0.3...v3.0.4 552 | [3.0.3]: https://github.com/jlmakes/scrollreveal/compare/v3.0.2...v3.0.3 553 | [3.0.2]: https://github.com/jlmakes/scrollreveal/compare/v3.0.1...v3.0.2 554 | [3.0.1]: https://github.com/jlmakes/scrollreveal/compare/v3.0.0...v3.0.1 555 | [3.0.0]: https://github.com/jlmakes/scrollreveal/compare/v2.3.2...v3.0.0 556 | [2.3.2]: https://github.com/jlmakes/scrollreveal/compare/v2.3.1...v2.3.2 557 | [2.3.1]: https://github.com/jlmakes/scrollreveal/compare/v2.3.0...v2.3.1 558 | [2.3.0]: https://github.com/jlmakes/scrollreveal/compare/v2.2.0...v2.3.0 559 | [2.2.0]: https://github.com/jlmakes/scrollreveal/compare/v2.1.0...v2.2.0 560 | [2.1.0]: https://github.com/jlmakes/scrollreveal/compare/v2.0.5...v2.1.0 561 | [2.0.5]: https://github.com/jlmakes/scrollreveal/compare/v2.0.4...v2.0.5 562 | [2.0.4]: https://github.com/jlmakes/scrollreveal/compare/v2.0.3...v2.0.4 563 | [2.0.3]: https://github.com/jlmakes/scrollreveal/compare/v2.0.2...v2.0.3 564 | [2.0.2]: https://github.com/jlmakes/scrollreveal/compare/v2.0.1...v2.0.2 565 | [2.0.1]: https://github.com/jlmakes/scrollreveal/compare/v2.0.0...v2.0.1 566 | [2.0.0]: https://github.com/jlmakes/scrollreveal/tree/v2.0.0 567 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | <p align="center"> 2 | <a href="https://scrollrevealjs.org" title="Visit ScrollReveal home page"> 3 | <img src="https://scrollrevealjs.org/img/logomark.svg" alt="ScrollReveal" width="120"> 4 | </a> 5 | </p> 6 | <br> 7 | <p align="center"> 8 | <a href="https://scrollrevealjs.org" title="Visit ScrollReveal home page"> 9 | <img width="200" src="https://scrollrevealjs.org/img/scrollreveal-logotype-dark.svg" alt="ScrollReveal"> 10 | </a> 11 | </p> 12 | <p align="center">Animate elements as they scroll into view.</p> 13 | 14 | <p align="center"> 15 | <a href="https://travis-ci.org/jlmakes/scrollreveal"> 16 | <img src="https://img.shields.io/travis/jlmakes/scrollreveal.svg" alt="Build status"> 17 | </a> 18 | <a href="https://www.npmjs.com/package/scrollreveal"> 19 | <img src="https://img.shields.io/npm/dm/scrollreveal.svg" alt="Monthly downloads"> 20 | </a> 21 | <a href="https://www.npmjs.com/package/scrollreveal"> 22 | <img src="https://img.shields.io/npm/v/scrollreveal.svg" alt="Version"> 23 | </a> 24 | <img src="https://img.shields.io/badge/min+gzip-5.7_kB-blue.svg" alt="5.7 kB min+gzip"> 25 | <a href="https://opensource.org/licenses/GPL-3.0"> 26 | <img src="https://img.shields.io/badge/license-GPLv3-blue.svg" alt="GPLv3 License"> 27 | </a> 28 | </p> 29 | 30 | <br> 31 | 32 | # Installation 33 | 34 | ## Browser 35 | 36 | A simple and fast way to get started is to include this script on your page: 37 | 38 | ```html 39 | <script src="https://unpkg.com/scrollreveal"></script> 40 | ``` 41 | 42 | This will create the global variable `ScrollReveal` 43 | 44 | > Be careful using this method in production. Without specifying a fixed version number, Unpkg may delay your page load while it resolves the latest version. Learn more at [unpkg.com](https://unpkg.com) 45 | 46 | ## Module 47 | 48 | ```bash 49 | $ npm install scrollreveal 50 | ``` 51 | 52 | #### CommonJS 53 | 54 | ```js 55 | const ScrollReveal = require('scrollreveal') 56 | ``` 57 | 58 | #### ES2015 59 | 60 | ```js 61 | import ScrollReveal from 'scrollreveal' 62 | ``` 63 | 64 | <br> 65 | 66 | # Usage 67 | 68 | Installation provides us with the constructor function [`ScrollReveal()`](https://scrollrevealjs.org/api/constructor.html). Calling this function returns the ScrollReveal instance, the “brain” behind the magic. 69 | 70 | > ScrollReveal employs the singleton pattern; no matter how many times the constructor is called, it will always return the same instance. This means we can call it anywhere, worry-free. 71 | 72 | There’s a lot we can do with this instance, but most of the time we’ll be using the [`reveal()`](https://scrollrevealjs.org/api/reveal.html) method to create animation. Fundamentally, this is how to use ScrollReveal: 73 | 74 | ```html 75 | <h1 class="headline"> 76 | Widget Inc. 77 | </h1> 78 | ``` 79 | 80 | ```js 81 | ScrollReveal().reveal('.headline') 82 | ``` 83 | 84 | **🔎 See this demo live on [JSBin](http://jsbin.com/jufohaxonu/edit?html,output)** 85 | 86 | <br> 87 | 88 | --- 89 | 90 | ### The full documentation can be found at [https://scrollrevealjs.org](https://scrollrevealjs.org) 91 | 92 | > If you’re using an older version of ScrollReveal, you can find legacy documentation in the [wiki](https://github.com/jlmakes/scrollreveal/wiki) 93 | 94 | --- 95 | 96 | <br> 97 | 98 | <a href="https://scrollrevealjs.org/pricing/" title="Visit ScrollReveal pricing page"> 99 | <img align="right" height="300" src="https://scrollrevealjs.org/img/license.svg" alt="Commercial License Badge"> 100 | </a> 101 | 102 | <br> 103 | 104 | # License 105 | 106 | **For commercial sites, themes, projects, and applications, keep your source code private/proprietary by purchasing a [Commercial License](https://scrollrevealjs.org/pricing/).** 107 | 108 | Licensed under the GNU General Public License 3.0 for compatible open source projects and non-commercial use. 109 | 110 | <br> 111 | 112 | Copyright 2023 Fisssion LLC 113 | -------------------------------------------------------------------------------- /build/rollup.conf.banner.js: -------------------------------------------------------------------------------- 1 | const { version } = require('../package.json') 2 | 3 | const banner = `/*! @license ScrollReveal v${version} 4 | 5 | Copyright 2021 Fisssion LLC. 6 | 7 | Licensed under the GNU General Public License 3.0 for 8 | compatible open source projects and non-commercial use. 9 | 10 | For commercial sites, themes, projects, and applications, 11 | keep your source code private/proprietary by purchasing 12 | a commercial license from https://scrollrevealjs.org/ 13 | */` 14 | 15 | export default banner 16 | -------------------------------------------------------------------------------- /build/rollup.conf.js: -------------------------------------------------------------------------------- 1 | import buble from 'rollup-plugin-buble' 2 | import json from 'rollup-plugin-json' 3 | import pkg from '../package.json' 4 | import nodeResolve from 'rollup-plugin-node-resolve' 5 | import banner from './rollup.conf.banner' 6 | 7 | const base = { 8 | input: './src/index.js', 9 | plugins: [json(), nodeResolve(), buble()] 10 | } 11 | 12 | const es = Object.assign({}, base, { 13 | external: [...Object.keys(pkg.dependencies || {})], 14 | output: { banner, format: 'es', file: './dist/scrollreveal.es.js' } 15 | }) 16 | 17 | const umd = Object.assign({}, base, { 18 | output: { 19 | banner, 20 | format: 'umd', 21 | file: './dist/scrollreveal.js', 22 | name: 'ScrollReveal' 23 | } 24 | }) 25 | 26 | export default [es, umd] 27 | -------------------------------------------------------------------------------- /build/rollup.conf.min.js: -------------------------------------------------------------------------------- 1 | import buble from 'rollup-plugin-buble' 2 | import json from 'rollup-plugin-json' 3 | import nodeResolve from 'rollup-plugin-node-resolve' 4 | import strip from 'rollup-plugin-strip' 5 | import { uglify } from 'rollup-plugin-uglify' 6 | import banner from './rollup.conf.banner' 7 | 8 | export default { 9 | input: 'src/index.js', 10 | plugins: [ 11 | json(), 12 | nodeResolve(), 13 | buble(), 14 | strip({ 15 | functions: ['logger'], 16 | sourceMaps: false 17 | }), 18 | uglify({ 19 | output: { 20 | comments: /@license ScrollReveal/ 21 | } 22 | }) 23 | ], 24 | output: { 25 | banner, 26 | format: 'iife', 27 | file: 'dist/scrollreveal.min.js', 28 | name: 'ScrollReveal' 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /dist/scrollreveal.es.js: -------------------------------------------------------------------------------- 1 | /*! @license ScrollReveal v4.0.9 2 | 3 | Copyright 2021 Fisssion LLC. 4 | 5 | Licensed under the GNU General Public License 3.0 for 6 | compatible open source projects and non-commercial use. 7 | 8 | For commercial sites, themes, projects, and applications, 9 | keep your source code private/proprietary by purchasing 10 | a commercial license from https://scrollrevealjs.org/ 11 | */ 12 | import $ from 'tealight'; 13 | import { translateY, translateX, rotateX, rotateY, rotateZ, scale, parse, multiply } from 'rematrix'; 14 | import raf from 'miniraf'; 15 | 16 | var defaults = { 17 | delay: 0, 18 | distance: '0', 19 | duration: 600, 20 | easing: 'cubic-bezier(0.5, 0, 0, 1)', 21 | interval: 0, 22 | opacity: 0, 23 | origin: 'bottom', 24 | rotate: { 25 | x: 0, 26 | y: 0, 27 | z: 0 28 | }, 29 | scale: 1, 30 | cleanup: false, 31 | container: document.documentElement, 32 | desktop: true, 33 | mobile: true, 34 | reset: false, 35 | useDelay: 'always', 36 | viewFactor: 0.0, 37 | viewOffset: { 38 | top: 0, 39 | right: 0, 40 | bottom: 0, 41 | left: 0 42 | }, 43 | afterReset: function afterReset() {}, 44 | afterReveal: function afterReveal() {}, 45 | beforeReset: function beforeReset() {}, 46 | beforeReveal: function beforeReveal() {} 47 | }; 48 | 49 | function failure() { 50 | document.documentElement.classList.remove('sr'); 51 | 52 | return { 53 | clean: function clean() {}, 54 | destroy: function destroy() {}, 55 | reveal: function reveal() {}, 56 | sync: function sync() {}, 57 | get noop() { 58 | return true 59 | } 60 | } 61 | } 62 | 63 | function success() { 64 | document.documentElement.classList.add('sr'); 65 | 66 | if (document.body) { 67 | document.body.style.height = '100%'; 68 | } else { 69 | document.addEventListener('DOMContentLoaded', function () { 70 | document.body.style.height = '100%'; 71 | }); 72 | } 73 | } 74 | 75 | var mount = { success: success, failure: failure }; 76 | 77 | function isObject(x) { 78 | return ( 79 | x !== null && 80 | x instanceof Object && 81 | (x.constructor === Object || 82 | Object.prototype.toString.call(x) === '[object Object]') 83 | ) 84 | } 85 | 86 | function each(collection, callback) { 87 | if (isObject(collection)) { 88 | var keys = Object.keys(collection); 89 | return keys.forEach(function (key) { return callback(collection[key], key, collection); }) 90 | } 91 | if (collection instanceof Array) { 92 | return collection.forEach(function (item, i) { return callback(item, i, collection); }) 93 | } 94 | throw new TypeError('Expected either an array or object literal.') 95 | } 96 | 97 | function logger(message) { 98 | var details = [], len = arguments.length - 1; 99 | while ( len-- > 0 ) details[ len ] = arguments[ len + 1 ]; 100 | 101 | if (this.constructor.debug && console) { 102 | var report = "%cScrollReveal: " + message; 103 | details.forEach(function (detail) { return (report += "\n — " + detail); }); 104 | console.log(report, 'color: #ea654b;'); // eslint-disable-line no-console 105 | } 106 | } 107 | 108 | function rinse() { 109 | var this$1 = this; 110 | 111 | var struct = function () { return ({ 112 | active: [], 113 | stale: [] 114 | }); }; 115 | 116 | var elementIds = struct(); 117 | var sequenceIds = struct(); 118 | var containerIds = struct(); 119 | 120 | /** 121 | * Take stock of active element IDs. 122 | */ 123 | try { 124 | each($('[data-sr-id]'), function (node) { 125 | var id = parseInt(node.getAttribute('data-sr-id')); 126 | elementIds.active.push(id); 127 | }); 128 | } catch (e) { 129 | throw e 130 | } 131 | /** 132 | * Destroy stale elements. 133 | */ 134 | each(this.store.elements, function (element) { 135 | if (elementIds.active.indexOf(element.id) === -1) { 136 | elementIds.stale.push(element.id); 137 | } 138 | }); 139 | 140 | each(elementIds.stale, function (staleId) { return delete this$1.store.elements[staleId]; }); 141 | 142 | /** 143 | * Take stock of active container and sequence IDs. 144 | */ 145 | each(this.store.elements, function (element) { 146 | if (containerIds.active.indexOf(element.containerId) === -1) { 147 | containerIds.active.push(element.containerId); 148 | } 149 | if (element.hasOwnProperty('sequence')) { 150 | if (sequenceIds.active.indexOf(element.sequence.id) === -1) { 151 | sequenceIds.active.push(element.sequence.id); 152 | } 153 | } 154 | }); 155 | 156 | /** 157 | * Destroy stale containers. 158 | */ 159 | each(this.store.containers, function (container) { 160 | if (containerIds.active.indexOf(container.id) === -1) { 161 | containerIds.stale.push(container.id); 162 | } 163 | }); 164 | 165 | each(containerIds.stale, function (staleId) { 166 | var stale = this$1.store.containers[staleId].node; 167 | stale.removeEventListener('scroll', this$1.delegate); 168 | stale.removeEventListener('resize', this$1.delegate); 169 | delete this$1.store.containers[staleId]; 170 | }); 171 | 172 | /** 173 | * Destroy stale sequences. 174 | */ 175 | each(this.store.sequences, function (sequence) { 176 | if (sequenceIds.active.indexOf(sequence.id) === -1) { 177 | sequenceIds.stale.push(sequence.id); 178 | } 179 | }); 180 | 181 | each(sequenceIds.stale, function (staleId) { return delete this$1.store.sequences[staleId]; }); 182 | } 183 | 184 | var getPrefixedCssProp = (function () { 185 | var properties = {}; 186 | var style = document.documentElement.style; 187 | 188 | function getPrefixedCssProperty(name, source) { 189 | if ( source === void 0 ) source = style; 190 | 191 | if (name && typeof name === 'string') { 192 | if (properties[name]) { 193 | return properties[name] 194 | } 195 | if (typeof source[name] === 'string') { 196 | return (properties[name] = name) 197 | } 198 | if (typeof source[("-webkit-" + name)] === 'string') { 199 | return (properties[name] = "-webkit-" + name) 200 | } 201 | throw new RangeError(("Unable to find \"" + name + "\" style property.")) 202 | } 203 | throw new TypeError('Expected a string.') 204 | } 205 | 206 | getPrefixedCssProperty.clearCache = function () { return (properties = {}); }; 207 | 208 | return getPrefixedCssProperty 209 | })(); 210 | 211 | function style(element) { 212 | var computed = window.getComputedStyle(element.node); 213 | var position = computed.position; 214 | var config = element.config; 215 | 216 | /** 217 | * Generate inline styles 218 | */ 219 | var inline = {}; 220 | var inlineStyle = element.node.getAttribute('style') || ''; 221 | var inlineMatch = inlineStyle.match(/[\w-]+\s*:\s*[^;]+\s*/gi) || []; 222 | 223 | inline.computed = inlineMatch ? inlineMatch.map(function (m) { return m.trim(); }).join('; ') + ';' : ''; 224 | 225 | inline.generated = inlineMatch.some(function (m) { return m.match(/visibility\s?:\s?visible/i); }) 226 | ? inline.computed 227 | : inlineMatch.concat( ['visibility: visible']).map(function (m) { return m.trim(); }).join('; ') + ';'; 228 | 229 | /** 230 | * Generate opacity styles 231 | */ 232 | var computedOpacity = parseFloat(computed.opacity); 233 | var configOpacity = !isNaN(parseFloat(config.opacity)) 234 | ? parseFloat(config.opacity) 235 | : parseFloat(computed.opacity); 236 | 237 | var opacity = { 238 | computed: computedOpacity !== configOpacity ? ("opacity: " + computedOpacity + ";") : '', 239 | generated: computedOpacity !== configOpacity ? ("opacity: " + configOpacity + ";") : '' 240 | }; 241 | 242 | /** 243 | * Generate transformation styles 244 | */ 245 | var transformations = []; 246 | 247 | if (parseFloat(config.distance)) { 248 | var axis = config.origin === 'top' || config.origin === 'bottom' ? 'Y' : 'X'; 249 | 250 | /** 251 | * Let’s make sure our our pixel distances are negative for top and left. 252 | * e.g. { origin: 'top', distance: '25px' } starts at `top: -25px` in CSS. 253 | */ 254 | var distance = config.distance; 255 | if (config.origin === 'top' || config.origin === 'left') { 256 | distance = /^-/.test(distance) ? distance.substr(1) : ("-" + distance); 257 | } 258 | 259 | var ref = distance.match(/(^-?\d+\.?\d?)|(em$|px$|%$)/g); 260 | var value = ref[0]; 261 | var unit = ref[1]; 262 | 263 | switch (unit) { 264 | case 'em': 265 | distance = parseInt(computed.fontSize) * value; 266 | break 267 | case 'px': 268 | distance = value; 269 | break 270 | case '%': 271 | /** 272 | * Here we use `getBoundingClientRect` instead of 273 | * the existing data attached to `element.geometry` 274 | * because only the former includes any transformations 275 | * current applied to the element. 276 | * 277 | * If that behavior ends up being unintuitive, this 278 | * logic could instead utilize `element.geometry.height` 279 | * and `element.geoemetry.width` for the distance calculation 280 | */ 281 | distance = 282 | axis === 'Y' 283 | ? (element.node.getBoundingClientRect().height * value) / 100 284 | : (element.node.getBoundingClientRect().width * value) / 100; 285 | break 286 | default: 287 | throw new RangeError('Unrecognized or missing distance unit.') 288 | } 289 | 290 | if (axis === 'Y') { 291 | transformations.push(translateY(distance)); 292 | } else { 293 | transformations.push(translateX(distance)); 294 | } 295 | } 296 | 297 | if (config.rotate.x) { transformations.push(rotateX(config.rotate.x)); } 298 | if (config.rotate.y) { transformations.push(rotateY(config.rotate.y)); } 299 | if (config.rotate.z) { transformations.push(rotateZ(config.rotate.z)); } 300 | if (config.scale !== 1) { 301 | if (config.scale === 0) { 302 | /** 303 | * The CSS Transforms matrix interpolation specification 304 | * basically disallows transitions of non-invertible 305 | * matrixes, which means browsers won't transition 306 | * elements with zero scale. 307 | * 308 | * That’s inconvenient for the API and developer 309 | * experience, so we simply nudge their value 310 | * slightly above zero; this allows browsers 311 | * to transition our element as expected. 312 | * 313 | * `0.0002` was the smallest number 314 | * that performed across browsers. 315 | */ 316 | transformations.push(scale(0.0002)); 317 | } else { 318 | transformations.push(scale(config.scale)); 319 | } 320 | } 321 | 322 | var transform = {}; 323 | if (transformations.length) { 324 | transform.property = getPrefixedCssProp('transform'); 325 | /** 326 | * The default computed transform value should be one of: 327 | * undefined || 'none' || 'matrix()' || 'matrix3d()' 328 | */ 329 | transform.computed = { 330 | raw: computed[transform.property], 331 | matrix: parse(computed[transform.property]) 332 | }; 333 | 334 | transformations.unshift(transform.computed.matrix); 335 | var product = transformations.reduce(multiply); 336 | 337 | transform.generated = { 338 | initial: ((transform.property) + ": matrix3d(" + (product.join(', ')) + ");"), 339 | final: ((transform.property) + ": matrix3d(" + (transform.computed.matrix.join(', ')) + ");") 340 | }; 341 | } else { 342 | transform.generated = { 343 | initial: '', 344 | final: '' 345 | }; 346 | } 347 | 348 | /** 349 | * Generate transition styles 350 | */ 351 | var transition = {}; 352 | if (opacity.generated || transform.generated.initial) { 353 | transition.property = getPrefixedCssProp('transition'); 354 | transition.computed = computed[transition.property]; 355 | transition.fragments = []; 356 | 357 | var delay = config.delay; 358 | var duration = config.duration; 359 | var easing = config.easing; 360 | 361 | if (opacity.generated) { 362 | transition.fragments.push({ 363 | delayed: ("opacity " + (duration / 1000) + "s " + easing + " " + (delay / 1000) + "s"), 364 | instant: ("opacity " + (duration / 1000) + "s " + easing + " 0s") 365 | }); 366 | } 367 | 368 | if (transform.generated.initial) { 369 | transition.fragments.push({ 370 | delayed: ((transform.property) + " " + (duration / 1000) + "s " + easing + " " + (delay / 1000) + "s"), 371 | instant: ((transform.property) + " " + (duration / 1000) + "s " + easing + " 0s") 372 | }); 373 | } 374 | 375 | /** 376 | * The default computed transition property should be undefined, or one of: 377 | * '' || 'none 0s ease 0s' || 'all 0s ease 0s' || 'all 0s 0s cubic-bezier()' 378 | */ 379 | var hasCustomTransition = 380 | transition.computed && !transition.computed.match(/all 0s|none 0s/); 381 | 382 | if (hasCustomTransition) { 383 | transition.fragments.unshift({ 384 | delayed: transition.computed, 385 | instant: transition.computed 386 | }); 387 | } 388 | 389 | var composed = transition.fragments.reduce( 390 | function (composition, fragment, i) { 391 | composition.delayed += i === 0 ? fragment.delayed : (", " + (fragment.delayed)); 392 | composition.instant += i === 0 ? fragment.instant : (", " + (fragment.instant)); 393 | return composition 394 | }, 395 | { 396 | delayed: '', 397 | instant: '' 398 | } 399 | ); 400 | 401 | transition.generated = { 402 | delayed: ((transition.property) + ": " + (composed.delayed) + ";"), 403 | instant: ((transition.property) + ": " + (composed.instant) + ";") 404 | }; 405 | } else { 406 | transition.generated = { 407 | delayed: '', 408 | instant: '' 409 | }; 410 | } 411 | 412 | return { 413 | inline: inline, 414 | opacity: opacity, 415 | position: position, 416 | transform: transform, 417 | transition: transition 418 | } 419 | } 420 | 421 | /** 422 | * apply a CSS string to an element using the CSSOM (element.style) rather 423 | * than setAttribute, which may violate the content security policy. 424 | * 425 | * @param {Node} [el] Element to receive styles. 426 | * @param {string} [declaration] Styles to apply. 427 | */ 428 | function applyStyle (el, declaration) { 429 | declaration.split(';').forEach(function (pair) { 430 | var ref = pair.split(':'); 431 | var property = ref[0]; 432 | var value = ref.slice(1); 433 | if (property && value) { 434 | el.style[property.trim()] = value.join(':'); 435 | } 436 | }); 437 | } 438 | 439 | function clean(target) { 440 | var this$1 = this; 441 | 442 | var dirty; 443 | try { 444 | each($(target), function (node) { 445 | var id = node.getAttribute('data-sr-id'); 446 | if (id !== null) { 447 | dirty = true; 448 | var element = this$1.store.elements[id]; 449 | if (element.callbackTimer) { 450 | window.clearTimeout(element.callbackTimer.clock); 451 | } 452 | applyStyle(element.node, element.styles.inline.generated); 453 | node.removeAttribute('data-sr-id'); 454 | delete this$1.store.elements[id]; 455 | } 456 | }); 457 | } catch (e) { 458 | return logger.call(this, 'Clean failed.', e.message) 459 | } 460 | 461 | if (dirty) { 462 | try { 463 | rinse.call(this); 464 | } catch (e) { 465 | return logger.call(this, 'Clean failed.', e.message) 466 | } 467 | } 468 | } 469 | 470 | function destroy() { 471 | var this$1 = this; 472 | 473 | /** 474 | * Remove all generated styles and element ids 475 | */ 476 | each(this.store.elements, function (element) { 477 | applyStyle(element.node, element.styles.inline.generated); 478 | element.node.removeAttribute('data-sr-id'); 479 | }); 480 | 481 | /** 482 | * Remove all event listeners. 483 | */ 484 | each(this.store.containers, function (container) { 485 | var target = 486 | container.node === document.documentElement ? window : container.node; 487 | target.removeEventListener('scroll', this$1.delegate); 488 | target.removeEventListener('resize', this$1.delegate); 489 | }); 490 | 491 | /** 492 | * Clear all data from the store 493 | */ 494 | this.store = { 495 | containers: {}, 496 | elements: {}, 497 | history: [], 498 | sequences: {} 499 | }; 500 | } 501 | 502 | function deepAssign(target) { 503 | var sources = [], len = arguments.length - 1; 504 | while ( len-- > 0 ) sources[ len ] = arguments[ len + 1 ]; 505 | 506 | if (isObject(target)) { 507 | each(sources, function (source) { 508 | each(source, function (data, key) { 509 | if (isObject(data)) { 510 | if (!target[key] || !isObject(target[key])) { 511 | target[key] = {}; 512 | } 513 | deepAssign(target[key], data); 514 | } else { 515 | target[key] = data; 516 | } 517 | }); 518 | }); 519 | return target 520 | } else { 521 | throw new TypeError('Target must be an object literal.') 522 | } 523 | } 524 | 525 | function isMobile(agent) { 526 | if ( agent === void 0 ) agent = navigator.userAgent; 527 | 528 | return /Android|iPhone|iPad|iPod/i.test(agent) 529 | } 530 | 531 | var nextUniqueId = (function () { 532 | var uid = 0; 533 | return function () { return uid++; } 534 | })(); 535 | 536 | function initialize() { 537 | var this$1 = this; 538 | 539 | rinse.call(this); 540 | 541 | each(this.store.elements, function (element) { 542 | var styles = [element.styles.inline.generated]; 543 | 544 | if (element.visible) { 545 | styles.push(element.styles.opacity.computed); 546 | styles.push(element.styles.transform.generated.final); 547 | element.revealed = true; 548 | } else { 549 | styles.push(element.styles.opacity.generated); 550 | styles.push(element.styles.transform.generated.initial); 551 | element.revealed = false; 552 | } 553 | 554 | applyStyle(element.node, styles.filter(function (s) { return s !== ''; }).join(' ')); 555 | }); 556 | 557 | each(this.store.containers, function (container) { 558 | var target = 559 | container.node === document.documentElement ? window : container.node; 560 | target.addEventListener('scroll', this$1.delegate); 561 | target.addEventListener('resize', this$1.delegate); 562 | }); 563 | 564 | /** 565 | * Manually invoke delegate once to capture 566 | * element and container dimensions, container 567 | * scroll position, and trigger any valid reveals 568 | */ 569 | this.delegate(); 570 | 571 | /** 572 | * Wipe any existing `setTimeout` now 573 | * that initialization has completed. 574 | */ 575 | this.initTimeout = null; 576 | } 577 | 578 | function animate(element, force) { 579 | if ( force === void 0 ) force = {}; 580 | 581 | var pristine = force.pristine || this.pristine; 582 | var delayed = 583 | element.config.useDelay === 'always' || 584 | (element.config.useDelay === 'onload' && pristine) || 585 | (element.config.useDelay === 'once' && !element.seen); 586 | 587 | var shouldReveal = element.visible && !element.revealed; 588 | var shouldReset = !element.visible && element.revealed && element.config.reset; 589 | 590 | if (force.reveal || shouldReveal) { 591 | return triggerReveal.call(this, element, delayed) 592 | } 593 | 594 | if (force.reset || shouldReset) { 595 | return triggerReset.call(this, element) 596 | } 597 | } 598 | 599 | function triggerReveal(element, delayed) { 600 | var styles = [ 601 | element.styles.inline.generated, 602 | element.styles.opacity.computed, 603 | element.styles.transform.generated.final 604 | ]; 605 | if (delayed) { 606 | styles.push(element.styles.transition.generated.delayed); 607 | } else { 608 | styles.push(element.styles.transition.generated.instant); 609 | } 610 | element.revealed = element.seen = true; 611 | applyStyle(element.node, styles.filter(function (s) { return s !== ''; }).join(' ')); 612 | registerCallbacks.call(this, element, delayed); 613 | } 614 | 615 | function triggerReset(element) { 616 | var styles = [ 617 | element.styles.inline.generated, 618 | element.styles.opacity.generated, 619 | element.styles.transform.generated.initial, 620 | element.styles.transition.generated.instant 621 | ]; 622 | element.revealed = false; 623 | applyStyle(element.node, styles.filter(function (s) { return s !== ''; }).join(' ')); 624 | registerCallbacks.call(this, element); 625 | } 626 | 627 | function registerCallbacks(element, isDelayed) { 628 | var this$1 = this; 629 | 630 | var duration = isDelayed 631 | ? element.config.duration + element.config.delay 632 | : element.config.duration; 633 | 634 | var beforeCallback = element.revealed 635 | ? element.config.beforeReveal 636 | : element.config.beforeReset; 637 | 638 | var afterCallback = element.revealed 639 | ? element.config.afterReveal 640 | : element.config.afterReset; 641 | 642 | var elapsed = 0; 643 | if (element.callbackTimer) { 644 | elapsed = Date.now() - element.callbackTimer.start; 645 | window.clearTimeout(element.callbackTimer.clock); 646 | } 647 | 648 | beforeCallback(element.node); 649 | 650 | element.callbackTimer = { 651 | start: Date.now(), 652 | clock: window.setTimeout(function () { 653 | afterCallback(element.node); 654 | element.callbackTimer = null; 655 | if (element.revealed && !element.config.reset && element.config.cleanup) { 656 | clean.call(this$1, element.node); 657 | } 658 | }, duration - elapsed) 659 | }; 660 | } 661 | 662 | function sequence(element, pristine) { 663 | if ( pristine === void 0 ) pristine = this.pristine; 664 | 665 | /** 666 | * We first check if the element should reset. 667 | */ 668 | if (!element.visible && element.revealed && element.config.reset) { 669 | return animate.call(this, element, { reset: true }) 670 | } 671 | 672 | var seq = this.store.sequences[element.sequence.id]; 673 | var i = element.sequence.index; 674 | 675 | if (seq) { 676 | var visible = new SequenceModel(seq, 'visible', this.store); 677 | var revealed = new SequenceModel(seq, 'revealed', this.store); 678 | 679 | seq.models = { visible: visible, revealed: revealed }; 680 | 681 | /** 682 | * If the sequence has no revealed members, 683 | * then we reveal the first visible element 684 | * within that sequence. 685 | * 686 | * The sequence then cues a recursive call 687 | * in both directions. 688 | */ 689 | if (!revealed.body.length) { 690 | var nextId = seq.members[visible.body[0]]; 691 | var nextElement = this.store.elements[nextId]; 692 | 693 | if (nextElement) { 694 | cue.call(this, seq, visible.body[0], -1, pristine); 695 | cue.call(this, seq, visible.body[0], +1, pristine); 696 | return animate.call(this, nextElement, { reveal: true, pristine: pristine }) 697 | } 698 | } 699 | 700 | /** 701 | * If our element isn’t resetting, we check the 702 | * element sequence index against the head, and 703 | * then the foot of the sequence. 704 | */ 705 | if ( 706 | !seq.blocked.head && 707 | i === [].concat( revealed.head ).pop() && 708 | i >= [].concat( visible.body ).shift() 709 | ) { 710 | cue.call(this, seq, i, -1, pristine); 711 | return animate.call(this, element, { reveal: true, pristine: pristine }) 712 | } 713 | 714 | if ( 715 | !seq.blocked.foot && 716 | i === [].concat( revealed.foot ).shift() && 717 | i <= [].concat( visible.body ).pop() 718 | ) { 719 | cue.call(this, seq, i, +1, pristine); 720 | return animate.call(this, element, { reveal: true, pristine: pristine }) 721 | } 722 | } 723 | } 724 | 725 | function Sequence(interval) { 726 | var i = Math.abs(interval); 727 | if (!isNaN(i)) { 728 | this.id = nextUniqueId(); 729 | this.interval = Math.max(i, 16); 730 | this.members = []; 731 | this.models = {}; 732 | this.blocked = { 733 | head: false, 734 | foot: false 735 | }; 736 | } else { 737 | throw new RangeError('Invalid sequence interval.') 738 | } 739 | } 740 | 741 | function SequenceModel(seq, prop, store) { 742 | var this$1 = this; 743 | 744 | this.head = []; 745 | this.body = []; 746 | this.foot = []; 747 | 748 | each(seq.members, function (id, index) { 749 | var element = store.elements[id]; 750 | if (element && element[prop]) { 751 | this$1.body.push(index); 752 | } 753 | }); 754 | 755 | if (this.body.length) { 756 | each(seq.members, function (id, index) { 757 | var element = store.elements[id]; 758 | if (element && !element[prop]) { 759 | if (index < this$1.body[0]) { 760 | this$1.head.push(index); 761 | } else { 762 | this$1.foot.push(index); 763 | } 764 | } 765 | }); 766 | } 767 | } 768 | 769 | function cue(seq, i, direction, pristine) { 770 | var this$1 = this; 771 | 772 | var blocked = ['head', null, 'foot'][1 + direction]; 773 | var nextId = seq.members[i + direction]; 774 | var nextElement = this.store.elements[nextId]; 775 | 776 | seq.blocked[blocked] = true; 777 | 778 | setTimeout(function () { 779 | seq.blocked[blocked] = false; 780 | if (nextElement) { 781 | sequence.call(this$1, nextElement, pristine); 782 | } 783 | }, seq.interval); 784 | } 785 | 786 | function reveal(target, options, syncing) { 787 | var this$1 = this; 788 | if ( options === void 0 ) options = {}; 789 | if ( syncing === void 0 ) syncing = false; 790 | 791 | var containerBuffer = []; 792 | var sequence$1; 793 | var interval = options.interval || defaults.interval; 794 | 795 | try { 796 | if (interval) { 797 | sequence$1 = new Sequence(interval); 798 | } 799 | 800 | var nodes = $(target); 801 | if (!nodes.length) { 802 | throw new Error('Invalid reveal target.') 803 | } 804 | 805 | var elements = nodes.reduce(function (elementBuffer, elementNode) { 806 | var element = {}; 807 | var existingId = elementNode.getAttribute('data-sr-id'); 808 | 809 | if (existingId) { 810 | deepAssign(element, this$1.store.elements[existingId]); 811 | 812 | /** 813 | * In order to prevent previously generated styles 814 | * from throwing off the new styles, the style tag 815 | * has to be reverted to its pre-reveal state. 816 | */ 817 | applyStyle(element.node, element.styles.inline.computed); 818 | } else { 819 | element.id = nextUniqueId(); 820 | element.node = elementNode; 821 | element.seen = false; 822 | element.revealed = false; 823 | element.visible = false; 824 | } 825 | 826 | var config = deepAssign({}, element.config || this$1.defaults, options); 827 | 828 | if ((!config.mobile && isMobile()) || (!config.desktop && !isMobile())) { 829 | if (existingId) { 830 | clean.call(this$1, element); 831 | } 832 | return elementBuffer // skip elements that are disabled 833 | } 834 | 835 | var containerNode = $(config.container)[0]; 836 | if (!containerNode) { 837 | throw new Error('Invalid container.') 838 | } 839 | if (!containerNode.contains(elementNode)) { 840 | return elementBuffer // skip elements found outside the container 841 | } 842 | 843 | var containerId; 844 | { 845 | containerId = getContainerId( 846 | containerNode, 847 | containerBuffer, 848 | this$1.store.containers 849 | ); 850 | if (containerId === null) { 851 | containerId = nextUniqueId(); 852 | containerBuffer.push({ id: containerId, node: containerNode }); 853 | } 854 | } 855 | 856 | element.config = config; 857 | element.containerId = containerId; 858 | element.styles = style(element); 859 | 860 | if (sequence$1) { 861 | element.sequence = { 862 | id: sequence$1.id, 863 | index: sequence$1.members.length 864 | }; 865 | sequence$1.members.push(element.id); 866 | } 867 | 868 | elementBuffer.push(element); 869 | return elementBuffer 870 | }, []); 871 | 872 | /** 873 | * Modifying the DOM via setAttribute needs to be handled 874 | * separately from reading computed styles in the map above 875 | * for the browser to batch DOM changes (limiting reflows) 876 | */ 877 | each(elements, function (element) { 878 | this$1.store.elements[element.id] = element; 879 | element.node.setAttribute('data-sr-id', element.id); 880 | }); 881 | } catch (e) { 882 | return logger.call(this, 'Reveal failed.', e.message) 883 | } 884 | 885 | /** 886 | * Now that element set-up is complete... 887 | * Let’s commit any container and sequence data we have to the store. 888 | */ 889 | each(containerBuffer, function (container) { 890 | this$1.store.containers[container.id] = { 891 | id: container.id, 892 | node: container.node 893 | }; 894 | }); 895 | if (sequence$1) { 896 | this.store.sequences[sequence$1.id] = sequence$1; 897 | } 898 | 899 | /** 900 | * If reveal wasn't invoked by sync, we want to 901 | * make sure to add this call to the history. 902 | */ 903 | if (syncing !== true) { 904 | this.store.history.push({ target: target, options: options }); 905 | 906 | /** 907 | * Push initialization to the event queue, giving 908 | * multiple reveal calls time to be interpreted. 909 | */ 910 | if (this.initTimeout) { 911 | window.clearTimeout(this.initTimeout); 912 | } 913 | this.initTimeout = window.setTimeout(initialize.bind(this), 0); 914 | } 915 | } 916 | 917 | function getContainerId(node) { 918 | var collections = [], len = arguments.length - 1; 919 | while ( len-- > 0 ) collections[ len ] = arguments[ len + 1 ]; 920 | 921 | var id = null; 922 | each(collections, function (collection) { 923 | each(collection, function (container) { 924 | if (id === null && container.node === node) { 925 | id = container.id; 926 | } 927 | }); 928 | }); 929 | return id 930 | } 931 | 932 | /** 933 | * Re-runs the reveal method for each record stored in history, 934 | * for capturing new content asynchronously loaded into the DOM. 935 | */ 936 | function sync() { 937 | var this$1 = this; 938 | 939 | each(this.store.history, function (record) { 940 | reveal.call(this$1, record.target, record.options, true); 941 | }); 942 | 943 | initialize.call(this); 944 | } 945 | 946 | var polyfill = function (x) { return (x > 0) - (x < 0) || +x; }; 947 | var mathSign = Math.sign || polyfill; 948 | 949 | function getGeometry(target, isContainer) { 950 | /** 951 | * We want to ignore padding and scrollbars for container elements. 952 | * More information here: https://goo.gl/vOZpbz 953 | */ 954 | var height = isContainer ? target.node.clientHeight : target.node.offsetHeight; 955 | var width = isContainer ? target.node.clientWidth : target.node.offsetWidth; 956 | 957 | var offsetTop = 0; 958 | var offsetLeft = 0; 959 | var node = target.node; 960 | 961 | do { 962 | if (!isNaN(node.offsetTop)) { 963 | offsetTop += node.offsetTop; 964 | } 965 | if (!isNaN(node.offsetLeft)) { 966 | offsetLeft += node.offsetLeft; 967 | } 968 | node = node.offsetParent; 969 | } while (node) 970 | 971 | return { 972 | bounds: { 973 | top: offsetTop, 974 | right: offsetLeft + width, 975 | bottom: offsetTop + height, 976 | left: offsetLeft 977 | }, 978 | height: height, 979 | width: width 980 | } 981 | } 982 | 983 | function getScrolled(container) { 984 | var top, left; 985 | if (container.node === document.documentElement) { 986 | top = window.pageYOffset; 987 | left = window.pageXOffset; 988 | } else { 989 | top = container.node.scrollTop; 990 | left = container.node.scrollLeft; 991 | } 992 | return { top: top, left: left } 993 | } 994 | 995 | function isElementVisible(element) { 996 | if ( element === void 0 ) element = {}; 997 | 998 | var container = this.store.containers[element.containerId]; 999 | if (!container) { return } 1000 | 1001 | var viewFactor = Math.max(0, Math.min(1, element.config.viewFactor)); 1002 | var viewOffset = element.config.viewOffset; 1003 | 1004 | var elementBounds = { 1005 | top: element.geometry.bounds.top + element.geometry.height * viewFactor, 1006 | right: element.geometry.bounds.right - element.geometry.width * viewFactor, 1007 | bottom: element.geometry.bounds.bottom - element.geometry.height * viewFactor, 1008 | left: element.geometry.bounds.left + element.geometry.width * viewFactor 1009 | }; 1010 | 1011 | var containerBounds = { 1012 | top: container.geometry.bounds.top + container.scroll.top + viewOffset.top, 1013 | right: container.geometry.bounds.right + container.scroll.left - viewOffset.right, 1014 | bottom: 1015 | container.geometry.bounds.bottom + container.scroll.top - viewOffset.bottom, 1016 | left: container.geometry.bounds.left + container.scroll.left + viewOffset.left 1017 | }; 1018 | 1019 | return ( 1020 | (elementBounds.top < containerBounds.bottom && 1021 | elementBounds.right > containerBounds.left && 1022 | elementBounds.bottom > containerBounds.top && 1023 | elementBounds.left < containerBounds.right) || 1024 | element.styles.position === 'fixed' 1025 | ) 1026 | } 1027 | 1028 | function delegate( 1029 | event, 1030 | elements 1031 | ) { 1032 | var this$1 = this; 1033 | if ( event === void 0 ) event = { type: 'init' }; 1034 | if ( elements === void 0 ) elements = this.store.elements; 1035 | 1036 | raf(function () { 1037 | var stale = event.type === 'init' || event.type === 'resize'; 1038 | 1039 | each(this$1.store.containers, function (container) { 1040 | if (stale) { 1041 | container.geometry = getGeometry.call(this$1, container, true); 1042 | } 1043 | var scroll = getScrolled.call(this$1, container); 1044 | if (container.scroll) { 1045 | container.direction = { 1046 | x: mathSign(scroll.left - container.scroll.left), 1047 | y: mathSign(scroll.top - container.scroll.top) 1048 | }; 1049 | } 1050 | container.scroll = scroll; 1051 | }); 1052 | 1053 | /** 1054 | * Due to how the sequencer is implemented, it’s 1055 | * important that we update the state of all 1056 | * elements, before any animation logic is 1057 | * evaluated (in the second loop below). 1058 | */ 1059 | each(elements, function (element) { 1060 | if (stale || element.geometry === undefined) { 1061 | element.geometry = getGeometry.call(this$1, element); 1062 | } 1063 | element.visible = isElementVisible.call(this$1, element); 1064 | }); 1065 | 1066 | each(elements, function (element) { 1067 | if (element.sequence) { 1068 | sequence.call(this$1, element); 1069 | } else { 1070 | animate.call(this$1, element); 1071 | } 1072 | }); 1073 | 1074 | this$1.pristine = false; 1075 | }); 1076 | } 1077 | 1078 | function isTransformSupported() { 1079 | var style = document.documentElement.style; 1080 | return 'transform' in style || 'WebkitTransform' in style 1081 | } 1082 | 1083 | function isTransitionSupported() { 1084 | var style = document.documentElement.style; 1085 | return 'transition' in style || 'WebkitTransition' in style 1086 | } 1087 | 1088 | var version = "4.0.9"; 1089 | 1090 | var boundDelegate; 1091 | var boundDestroy; 1092 | var boundReveal; 1093 | var boundClean; 1094 | var boundSync; 1095 | var config; 1096 | var debug; 1097 | var instance; 1098 | 1099 | function ScrollReveal(options) { 1100 | if ( options === void 0 ) options = {}; 1101 | 1102 | var invokedWithoutNew = 1103 | typeof this === 'undefined' || 1104 | Object.getPrototypeOf(this) !== ScrollReveal.prototype; 1105 | 1106 | if (invokedWithoutNew) { 1107 | return new ScrollReveal(options) 1108 | } 1109 | 1110 | if (!ScrollReveal.isSupported()) { 1111 | logger.call(this, 'Instantiation failed.', 'This browser is not supported.'); 1112 | return mount.failure() 1113 | } 1114 | 1115 | var buffer; 1116 | try { 1117 | buffer = config 1118 | ? deepAssign({}, config, options) 1119 | : deepAssign({}, defaults, options); 1120 | } catch (e) { 1121 | logger.call(this, 'Invalid configuration.', e.message); 1122 | return mount.failure() 1123 | } 1124 | 1125 | try { 1126 | var container = $(buffer.container)[0]; 1127 | if (!container) { 1128 | throw new Error('Invalid container.') 1129 | } 1130 | } catch (e) { 1131 | logger.call(this, e.message); 1132 | return mount.failure() 1133 | } 1134 | 1135 | config = buffer; 1136 | 1137 | if ((!config.mobile && isMobile()) || (!config.desktop && !isMobile())) { 1138 | logger.call( 1139 | this, 1140 | 'This device is disabled.', 1141 | ("desktop: " + (config.desktop)), 1142 | ("mobile: " + (config.mobile)) 1143 | ); 1144 | return mount.failure() 1145 | } 1146 | 1147 | mount.success(); 1148 | 1149 | this.store = { 1150 | containers: {}, 1151 | elements: {}, 1152 | history: [], 1153 | sequences: {} 1154 | }; 1155 | 1156 | this.pristine = true; 1157 | 1158 | boundDelegate = boundDelegate || delegate.bind(this); 1159 | boundDestroy = boundDestroy || destroy.bind(this); 1160 | boundReveal = boundReveal || reveal.bind(this); 1161 | boundClean = boundClean || clean.bind(this); 1162 | boundSync = boundSync || sync.bind(this); 1163 | 1164 | Object.defineProperty(this, 'delegate', { get: function () { return boundDelegate; } }); 1165 | Object.defineProperty(this, 'destroy', { get: function () { return boundDestroy; } }); 1166 | Object.defineProperty(this, 'reveal', { get: function () { return boundReveal; } }); 1167 | Object.defineProperty(this, 'clean', { get: function () { return boundClean; } }); 1168 | Object.defineProperty(this, 'sync', { get: function () { return boundSync; } }); 1169 | 1170 | Object.defineProperty(this, 'defaults', { get: function () { return config; } }); 1171 | Object.defineProperty(this, 'version', { get: function () { return version; } }); 1172 | Object.defineProperty(this, 'noop', { get: function () { return false; } }); 1173 | 1174 | return instance ? instance : (instance = this) 1175 | } 1176 | 1177 | ScrollReveal.isSupported = function () { return isTransformSupported() && isTransitionSupported(); }; 1178 | 1179 | Object.defineProperty(ScrollReveal, 'debug', { 1180 | get: function () { return debug || false; }, 1181 | set: function (value) { return (debug = typeof value === 'boolean' ? value : debug); } 1182 | }); 1183 | 1184 | ScrollReveal(); 1185 | 1186 | export default ScrollReveal; 1187 | -------------------------------------------------------------------------------- /dist/scrollreveal.js: -------------------------------------------------------------------------------- 1 | /*! @license ScrollReveal v4.0.9 2 | 3 | Copyright 2021 Fisssion LLC. 4 | 5 | Licensed under the GNU General Public License 3.0 for 6 | compatible open source projects and non-commercial use. 7 | 8 | For commercial sites, themes, projects, and applications, 9 | keep your source code private/proprietary by purchasing 10 | a commercial license from https://scrollrevealjs.org/ 11 | */ 12 | (function (global, factory) { 13 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 14 | typeof define === 'function' && define.amd ? define(factory) : 15 | (global = global || self, global.ScrollReveal = factory()); 16 | }(this, function () { 'use strict'; 17 | 18 | var defaults = { 19 | delay: 0, 20 | distance: '0', 21 | duration: 600, 22 | easing: 'cubic-bezier(0.5, 0, 0, 1)', 23 | interval: 0, 24 | opacity: 0, 25 | origin: 'bottom', 26 | rotate: { 27 | x: 0, 28 | y: 0, 29 | z: 0 30 | }, 31 | scale: 1, 32 | cleanup: false, 33 | container: document.documentElement, 34 | desktop: true, 35 | mobile: true, 36 | reset: false, 37 | useDelay: 'always', 38 | viewFactor: 0.0, 39 | viewOffset: { 40 | top: 0, 41 | right: 0, 42 | bottom: 0, 43 | left: 0 44 | }, 45 | afterReset: function afterReset() {}, 46 | afterReveal: function afterReveal() {}, 47 | beforeReset: function beforeReset() {}, 48 | beforeReveal: function beforeReveal() {} 49 | }; 50 | 51 | function failure() { 52 | document.documentElement.classList.remove('sr'); 53 | 54 | return { 55 | clean: function clean() {}, 56 | destroy: function destroy() {}, 57 | reveal: function reveal() {}, 58 | sync: function sync() {}, 59 | get noop() { 60 | return true 61 | } 62 | } 63 | } 64 | 65 | function success() { 66 | document.documentElement.classList.add('sr'); 67 | 68 | if (document.body) { 69 | document.body.style.height = '100%'; 70 | } else { 71 | document.addEventListener('DOMContentLoaded', function () { 72 | document.body.style.height = '100%'; 73 | }); 74 | } 75 | } 76 | 77 | var mount = { success: success, failure: failure }; 78 | 79 | /*! @license is-dom-node v1.0.4 80 | 81 | Copyright 2018 Fisssion LLC. 82 | 83 | Permission is hereby granted, free of charge, to any person obtaining a copy 84 | of this software and associated documentation files (the "Software"), to deal 85 | in the Software without restriction, including without limitation the rights 86 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 87 | copies of the Software, and to permit persons to whom the Software is 88 | furnished to do so, subject to the following conditions: 89 | 90 | The above copyright notice and this permission notice shall be included in all 91 | copies or substantial portions of the Software. 92 | 93 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 94 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 95 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 96 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 97 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 98 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 99 | SOFTWARE. 100 | 101 | */ 102 | function isDomNode(x) { 103 | return typeof window.Node === 'object' 104 | ? x instanceof window.Node 105 | : x !== null && 106 | typeof x === 'object' && 107 | typeof x.nodeType === 'number' && 108 | typeof x.nodeName === 'string' 109 | } 110 | 111 | /*! @license is-dom-node-list v1.2.1 112 | 113 | Copyright 2018 Fisssion LLC. 114 | 115 | Permission is hereby granted, free of charge, to any person obtaining a copy 116 | of this software and associated documentation files (the "Software"), to deal 117 | in the Software without restriction, including without limitation the rights 118 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 119 | copies of the Software, and to permit persons to whom the Software is 120 | furnished to do so, subject to the following conditions: 121 | 122 | The above copyright notice and this permission notice shall be included in all 123 | copies or substantial portions of the Software. 124 | 125 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 126 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 127 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 128 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 129 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 130 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 131 | SOFTWARE. 132 | 133 | */ 134 | 135 | function isDomNodeList(x) { 136 | var prototypeToString = Object.prototype.toString.call(x); 137 | var regex = /^\[object (HTMLCollection|NodeList|Object)\]$/; 138 | 139 | return typeof window.NodeList === 'object' 140 | ? x instanceof window.NodeList 141 | : x !== null && 142 | typeof x === 'object' && 143 | typeof x.length === 'number' && 144 | regex.test(prototypeToString) && 145 | (x.length === 0 || isDomNode(x[0])) 146 | } 147 | 148 | /*! @license Tealight v0.3.6 149 | 150 | Copyright 2018 Fisssion LLC. 151 | 152 | Permission is hereby granted, free of charge, to any person obtaining a copy 153 | of this software and associated documentation files (the "Software"), to deal 154 | in the Software without restriction, including without limitation the rights 155 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 156 | copies of the Software, and to permit persons to whom the Software is 157 | furnished to do so, subject to the following conditions: 158 | 159 | The above copyright notice and this permission notice shall be included in all 160 | copies or substantial portions of the Software. 161 | 162 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 163 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 164 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 165 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 166 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 167 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 168 | SOFTWARE. 169 | 170 | */ 171 | 172 | function tealight(target, context) { 173 | if ( context === void 0 ) { context = document; } 174 | 175 | if (target instanceof Array) { return target.filter(isDomNode); } 176 | if (isDomNode(target)) { return [target]; } 177 | if (isDomNodeList(target)) { return Array.prototype.slice.call(target); } 178 | if (typeof target === "string") { 179 | try { 180 | var query = context.querySelectorAll(target); 181 | return Array.prototype.slice.call(query); 182 | } catch (err) { 183 | return []; 184 | } 185 | } 186 | return []; 187 | } 188 | 189 | function isObject(x) { 190 | return ( 191 | x !== null && 192 | x instanceof Object && 193 | (x.constructor === Object || 194 | Object.prototype.toString.call(x) === '[object Object]') 195 | ) 196 | } 197 | 198 | function each(collection, callback) { 199 | if (isObject(collection)) { 200 | var keys = Object.keys(collection); 201 | return keys.forEach(function (key) { return callback(collection[key], key, collection); }) 202 | } 203 | if (collection instanceof Array) { 204 | return collection.forEach(function (item, i) { return callback(item, i, collection); }) 205 | } 206 | throw new TypeError('Expected either an array or object literal.') 207 | } 208 | 209 | function logger(message) { 210 | var details = [], len = arguments.length - 1; 211 | while ( len-- > 0 ) details[ len ] = arguments[ len + 1 ]; 212 | 213 | if (this.constructor.debug && console) { 214 | var report = "%cScrollReveal: " + message; 215 | details.forEach(function (detail) { return (report += "\n — " + detail); }); 216 | console.log(report, 'color: #ea654b;'); // eslint-disable-line no-console 217 | } 218 | } 219 | 220 | function rinse() { 221 | var this$1 = this; 222 | 223 | var struct = function () { return ({ 224 | active: [], 225 | stale: [] 226 | }); }; 227 | 228 | var elementIds = struct(); 229 | var sequenceIds = struct(); 230 | var containerIds = struct(); 231 | 232 | /** 233 | * Take stock of active element IDs. 234 | */ 235 | try { 236 | each(tealight('[data-sr-id]'), function (node) { 237 | var id = parseInt(node.getAttribute('data-sr-id')); 238 | elementIds.active.push(id); 239 | }); 240 | } catch (e) { 241 | throw e 242 | } 243 | /** 244 | * Destroy stale elements. 245 | */ 246 | each(this.store.elements, function (element) { 247 | if (elementIds.active.indexOf(element.id) === -1) { 248 | elementIds.stale.push(element.id); 249 | } 250 | }); 251 | 252 | each(elementIds.stale, function (staleId) { return delete this$1.store.elements[staleId]; }); 253 | 254 | /** 255 | * Take stock of active container and sequence IDs. 256 | */ 257 | each(this.store.elements, function (element) { 258 | if (containerIds.active.indexOf(element.containerId) === -1) { 259 | containerIds.active.push(element.containerId); 260 | } 261 | if (element.hasOwnProperty('sequence')) { 262 | if (sequenceIds.active.indexOf(element.sequence.id) === -1) { 263 | sequenceIds.active.push(element.sequence.id); 264 | } 265 | } 266 | }); 267 | 268 | /** 269 | * Destroy stale containers. 270 | */ 271 | each(this.store.containers, function (container) { 272 | if (containerIds.active.indexOf(container.id) === -1) { 273 | containerIds.stale.push(container.id); 274 | } 275 | }); 276 | 277 | each(containerIds.stale, function (staleId) { 278 | var stale = this$1.store.containers[staleId].node; 279 | stale.removeEventListener('scroll', this$1.delegate); 280 | stale.removeEventListener('resize', this$1.delegate); 281 | delete this$1.store.containers[staleId]; 282 | }); 283 | 284 | /** 285 | * Destroy stale sequences. 286 | */ 287 | each(this.store.sequences, function (sequence) { 288 | if (sequenceIds.active.indexOf(sequence.id) === -1) { 289 | sequenceIds.stale.push(sequence.id); 290 | } 291 | }); 292 | 293 | each(sequenceIds.stale, function (staleId) { return delete this$1.store.sequences[staleId]; }); 294 | } 295 | 296 | /*! @license Rematrix v0.3.0 297 | 298 | Copyright 2018 Julian Lloyd. 299 | 300 | Permission is hereby granted, free of charge, to any person obtaining a copy 301 | of this software and associated documentation files (the "Software"), to deal 302 | in the Software without restriction, including without limitation the rights 303 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 304 | copies of the Software, and to permit persons to whom the Software is 305 | furnished to do so, subject to the following conditions: 306 | 307 | The above copyright notice and this permission notice shall be included in 308 | all copies or substantial portions of the Software. 309 | 310 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 311 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 312 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 313 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 314 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 315 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 316 | THE SOFTWARE. 317 | */ 318 | /** 319 | * @module Rematrix 320 | */ 321 | 322 | /** 323 | * Transformation matrices in the browser come in two flavors: 324 | * 325 | * - `matrix` using 6 values (short) 326 | * - `matrix3d` using 16 values (long) 327 | * 328 | * This utility follows this [conversion guide](https://goo.gl/EJlUQ1) 329 | * to expand short form matrices to their equivalent long form. 330 | * 331 | * @param {array} source - Accepts both short and long form matrices. 332 | * @return {array} 333 | */ 334 | function format(source) { 335 | if (source.constructor !== Array) { 336 | throw new TypeError('Expected array.') 337 | } 338 | if (source.length === 16) { 339 | return source 340 | } 341 | if (source.length === 6) { 342 | var matrix = identity(); 343 | matrix[0] = source[0]; 344 | matrix[1] = source[1]; 345 | matrix[4] = source[2]; 346 | matrix[5] = source[3]; 347 | matrix[12] = source[4]; 348 | matrix[13] = source[5]; 349 | return matrix 350 | } 351 | throw new RangeError('Expected array with either 6 or 16 values.') 352 | } 353 | 354 | /** 355 | * Returns a matrix representing no transformation. The product of any matrix 356 | * multiplied by the identity matrix will be the original matrix. 357 | * 358 | * > **Tip:** Similar to how `5 * 1 === 5`, where `1` is the identity. 359 | * 360 | * @return {array} 361 | */ 362 | function identity() { 363 | var matrix = []; 364 | for (var i = 0; i < 16; i++) { 365 | i % 5 == 0 ? matrix.push(1) : matrix.push(0); 366 | } 367 | return matrix 368 | } 369 | 370 | /** 371 | * Returns a 4x4 matrix describing the combined transformations 372 | * of both arguments. 373 | * 374 | * > **Note:** Order is very important. For example, rotating 45° 375 | * along the Z-axis, followed by translating 500 pixels along the 376 | * Y-axis... is not the same as translating 500 pixels along the 377 | * Y-axis, followed by rotating 45° along on the Z-axis. 378 | * 379 | * @param {array} m - Accepts both short and long form matrices. 380 | * @param {array} x - Accepts both short and long form matrices. 381 | * @return {array} 382 | */ 383 | function multiply(m, x) { 384 | var fm = format(m); 385 | var fx = format(x); 386 | var product = []; 387 | 388 | for (var i = 0; i < 4; i++) { 389 | var row = [fm[i], fm[i + 4], fm[i + 8], fm[i + 12]]; 390 | for (var j = 0; j < 4; j++) { 391 | var k = j * 4; 392 | var col = [fx[k], fx[k + 1], fx[k + 2], fx[k + 3]]; 393 | var result = 394 | row[0] * col[0] + row[1] * col[1] + row[2] * col[2] + row[3] * col[3]; 395 | 396 | product[i + k] = result; 397 | } 398 | } 399 | 400 | return product 401 | } 402 | 403 | /** 404 | * Attempts to return a 4x4 matrix describing the CSS transform 405 | * matrix passed in, but will return the identity matrix as a 406 | * fallback. 407 | * 408 | * > **Tip:** This method is used to convert a CSS matrix (retrieved as a 409 | * `string` from computed styles) to its equivalent array format. 410 | * 411 | * @param {string} source - `matrix` or `matrix3d` CSS Transform value. 412 | * @return {array} 413 | */ 414 | function parse(source) { 415 | if (typeof source === 'string') { 416 | var match = source.match(/matrix(3d)?\(([^)]+)\)/); 417 | if (match) { 418 | var raw = match[2].split(', ').map(parseFloat); 419 | return format(raw) 420 | } 421 | } 422 | return identity() 423 | } 424 | 425 | /** 426 | * Returns a 4x4 matrix describing X-axis rotation. 427 | * 428 | * @param {number} angle - Measured in degrees. 429 | * @return {array} 430 | */ 431 | function rotateX(angle) { 432 | var theta = Math.PI / 180 * angle; 433 | var matrix = identity(); 434 | 435 | matrix[5] = matrix[10] = Math.cos(theta); 436 | matrix[6] = matrix[9] = Math.sin(theta); 437 | matrix[9] *= -1; 438 | 439 | return matrix 440 | } 441 | 442 | /** 443 | * Returns a 4x4 matrix describing Y-axis rotation. 444 | * 445 | * @param {number} angle - Measured in degrees. 446 | * @return {array} 447 | */ 448 | function rotateY(angle) { 449 | var theta = Math.PI / 180 * angle; 450 | var matrix = identity(); 451 | 452 | matrix[0] = matrix[10] = Math.cos(theta); 453 | matrix[2] = matrix[8] = Math.sin(theta); 454 | matrix[2] *= -1; 455 | 456 | return matrix 457 | } 458 | 459 | /** 460 | * Returns a 4x4 matrix describing Z-axis rotation. 461 | * 462 | * @param {number} angle - Measured in degrees. 463 | * @return {array} 464 | */ 465 | function rotateZ(angle) { 466 | var theta = Math.PI / 180 * angle; 467 | var matrix = identity(); 468 | 469 | matrix[0] = matrix[5] = Math.cos(theta); 470 | matrix[1] = matrix[4] = Math.sin(theta); 471 | matrix[4] *= -1; 472 | 473 | return matrix 474 | } 475 | 476 | /** 477 | * Returns a 4x4 matrix describing 2D scaling. The first argument 478 | * is used for both X and Y-axis scaling, unless an optional 479 | * second argument is provided to explicitly define Y-axis scaling. 480 | * 481 | * @param {number} scalar - Decimal multiplier. 482 | * @param {number} [scalarY] - Decimal multiplier. 483 | * @return {array} 484 | */ 485 | function scale(scalar, scalarY) { 486 | var matrix = identity(); 487 | 488 | matrix[0] = scalar; 489 | matrix[5] = typeof scalarY === 'number' ? scalarY : scalar; 490 | 491 | return matrix 492 | } 493 | 494 | /** 495 | * Returns a 4x4 matrix describing X-axis translation. 496 | * 497 | * @param {number} distance - Measured in pixels. 498 | * @return {array} 499 | */ 500 | function translateX(distance) { 501 | var matrix = identity(); 502 | matrix[12] = distance; 503 | return matrix 504 | } 505 | 506 | /** 507 | * Returns a 4x4 matrix describing Y-axis translation. 508 | * 509 | * @param {number} distance - Measured in pixels. 510 | * @return {array} 511 | */ 512 | function translateY(distance) { 513 | var matrix = identity(); 514 | matrix[13] = distance; 515 | return matrix 516 | } 517 | 518 | var getPrefixedCssProp = (function () { 519 | var properties = {}; 520 | var style = document.documentElement.style; 521 | 522 | function getPrefixedCssProperty(name, source) { 523 | if ( source === void 0 ) source = style; 524 | 525 | if (name && typeof name === 'string') { 526 | if (properties[name]) { 527 | return properties[name] 528 | } 529 | if (typeof source[name] === 'string') { 530 | return (properties[name] = name) 531 | } 532 | if (typeof source[("-webkit-" + name)] === 'string') { 533 | return (properties[name] = "-webkit-" + name) 534 | } 535 | throw new RangeError(("Unable to find \"" + name + "\" style property.")) 536 | } 537 | throw new TypeError('Expected a string.') 538 | } 539 | 540 | getPrefixedCssProperty.clearCache = function () { return (properties = {}); }; 541 | 542 | return getPrefixedCssProperty 543 | })(); 544 | 545 | function style(element) { 546 | var computed = window.getComputedStyle(element.node); 547 | var position = computed.position; 548 | var config = element.config; 549 | 550 | /** 551 | * Generate inline styles 552 | */ 553 | var inline = {}; 554 | var inlineStyle = element.node.getAttribute('style') || ''; 555 | var inlineMatch = inlineStyle.match(/[\w-]+\s*:\s*[^;]+\s*/gi) || []; 556 | 557 | inline.computed = inlineMatch ? inlineMatch.map(function (m) { return m.trim(); }).join('; ') + ';' : ''; 558 | 559 | inline.generated = inlineMatch.some(function (m) { return m.match(/visibility\s?:\s?visible/i); }) 560 | ? inline.computed 561 | : inlineMatch.concat( ['visibility: visible']).map(function (m) { return m.trim(); }).join('; ') + ';'; 562 | 563 | /** 564 | * Generate opacity styles 565 | */ 566 | var computedOpacity = parseFloat(computed.opacity); 567 | var configOpacity = !isNaN(parseFloat(config.opacity)) 568 | ? parseFloat(config.opacity) 569 | : parseFloat(computed.opacity); 570 | 571 | var opacity = { 572 | computed: computedOpacity !== configOpacity ? ("opacity: " + computedOpacity + ";") : '', 573 | generated: computedOpacity !== configOpacity ? ("opacity: " + configOpacity + ";") : '' 574 | }; 575 | 576 | /** 577 | * Generate transformation styles 578 | */ 579 | var transformations = []; 580 | 581 | if (parseFloat(config.distance)) { 582 | var axis = config.origin === 'top' || config.origin === 'bottom' ? 'Y' : 'X'; 583 | 584 | /** 585 | * Let’s make sure our our pixel distances are negative for top and left. 586 | * e.g. { origin: 'top', distance: '25px' } starts at `top: -25px` in CSS. 587 | */ 588 | var distance = config.distance; 589 | if (config.origin === 'top' || config.origin === 'left') { 590 | distance = /^-/.test(distance) ? distance.substr(1) : ("-" + distance); 591 | } 592 | 593 | var ref = distance.match(/(^-?\d+\.?\d?)|(em$|px$|%$)/g); 594 | var value = ref[0]; 595 | var unit = ref[1]; 596 | 597 | switch (unit) { 598 | case 'em': 599 | distance = parseInt(computed.fontSize) * value; 600 | break 601 | case 'px': 602 | distance = value; 603 | break 604 | case '%': 605 | /** 606 | * Here we use `getBoundingClientRect` instead of 607 | * the existing data attached to `element.geometry` 608 | * because only the former includes any transformations 609 | * current applied to the element. 610 | * 611 | * If that behavior ends up being unintuitive, this 612 | * logic could instead utilize `element.geometry.height` 613 | * and `element.geoemetry.width` for the distance calculation 614 | */ 615 | distance = 616 | axis === 'Y' 617 | ? (element.node.getBoundingClientRect().height * value) / 100 618 | : (element.node.getBoundingClientRect().width * value) / 100; 619 | break 620 | default: 621 | throw new RangeError('Unrecognized or missing distance unit.') 622 | } 623 | 624 | if (axis === 'Y') { 625 | transformations.push(translateY(distance)); 626 | } else { 627 | transformations.push(translateX(distance)); 628 | } 629 | } 630 | 631 | if (config.rotate.x) { transformations.push(rotateX(config.rotate.x)); } 632 | if (config.rotate.y) { transformations.push(rotateY(config.rotate.y)); } 633 | if (config.rotate.z) { transformations.push(rotateZ(config.rotate.z)); } 634 | if (config.scale !== 1) { 635 | if (config.scale === 0) { 636 | /** 637 | * The CSS Transforms matrix interpolation specification 638 | * basically disallows transitions of non-invertible 639 | * matrixes, which means browsers won't transition 640 | * elements with zero scale. 641 | * 642 | * That’s inconvenient for the API and developer 643 | * experience, so we simply nudge their value 644 | * slightly above zero; this allows browsers 645 | * to transition our element as expected. 646 | * 647 | * `0.0002` was the smallest number 648 | * that performed across browsers. 649 | */ 650 | transformations.push(scale(0.0002)); 651 | } else { 652 | transformations.push(scale(config.scale)); 653 | } 654 | } 655 | 656 | var transform = {}; 657 | if (transformations.length) { 658 | transform.property = getPrefixedCssProp('transform'); 659 | /** 660 | * The default computed transform value should be one of: 661 | * undefined || 'none' || 'matrix()' || 'matrix3d()' 662 | */ 663 | transform.computed = { 664 | raw: computed[transform.property], 665 | matrix: parse(computed[transform.property]) 666 | }; 667 | 668 | transformations.unshift(transform.computed.matrix); 669 | var product = transformations.reduce(multiply); 670 | 671 | transform.generated = { 672 | initial: ((transform.property) + ": matrix3d(" + (product.join(', ')) + ");"), 673 | final: ((transform.property) + ": matrix3d(" + (transform.computed.matrix.join(', ')) + ");") 674 | }; 675 | } else { 676 | transform.generated = { 677 | initial: '', 678 | final: '' 679 | }; 680 | } 681 | 682 | /** 683 | * Generate transition styles 684 | */ 685 | var transition = {}; 686 | if (opacity.generated || transform.generated.initial) { 687 | transition.property = getPrefixedCssProp('transition'); 688 | transition.computed = computed[transition.property]; 689 | transition.fragments = []; 690 | 691 | var delay = config.delay; 692 | var duration = config.duration; 693 | var easing = config.easing; 694 | 695 | if (opacity.generated) { 696 | transition.fragments.push({ 697 | delayed: ("opacity " + (duration / 1000) + "s " + easing + " " + (delay / 1000) + "s"), 698 | instant: ("opacity " + (duration / 1000) + "s " + easing + " 0s") 699 | }); 700 | } 701 | 702 | if (transform.generated.initial) { 703 | transition.fragments.push({ 704 | delayed: ((transform.property) + " " + (duration / 1000) + "s " + easing + " " + (delay / 1000) + "s"), 705 | instant: ((transform.property) + " " + (duration / 1000) + "s " + easing + " 0s") 706 | }); 707 | } 708 | 709 | /** 710 | * The default computed transition property should be undefined, or one of: 711 | * '' || 'none 0s ease 0s' || 'all 0s ease 0s' || 'all 0s 0s cubic-bezier()' 712 | */ 713 | var hasCustomTransition = 714 | transition.computed && !transition.computed.match(/all 0s|none 0s/); 715 | 716 | if (hasCustomTransition) { 717 | transition.fragments.unshift({ 718 | delayed: transition.computed, 719 | instant: transition.computed 720 | }); 721 | } 722 | 723 | var composed = transition.fragments.reduce( 724 | function (composition, fragment, i) { 725 | composition.delayed += i === 0 ? fragment.delayed : (", " + (fragment.delayed)); 726 | composition.instant += i === 0 ? fragment.instant : (", " + (fragment.instant)); 727 | return composition 728 | }, 729 | { 730 | delayed: '', 731 | instant: '' 732 | } 733 | ); 734 | 735 | transition.generated = { 736 | delayed: ((transition.property) + ": " + (composed.delayed) + ";"), 737 | instant: ((transition.property) + ": " + (composed.instant) + ";") 738 | }; 739 | } else { 740 | transition.generated = { 741 | delayed: '', 742 | instant: '' 743 | }; 744 | } 745 | 746 | return { 747 | inline: inline, 748 | opacity: opacity, 749 | position: position, 750 | transform: transform, 751 | transition: transition 752 | } 753 | } 754 | 755 | /** 756 | * apply a CSS string to an element using the CSSOM (element.style) rather 757 | * than setAttribute, which may violate the content security policy. 758 | * 759 | * @param {Node} [el] Element to receive styles. 760 | * @param {string} [declaration] Styles to apply. 761 | */ 762 | function applyStyle (el, declaration) { 763 | declaration.split(';').forEach(function (pair) { 764 | var ref = pair.split(':'); 765 | var property = ref[0]; 766 | var value = ref.slice(1); 767 | if (property && value) { 768 | el.style[property.trim()] = value.join(':'); 769 | } 770 | }); 771 | } 772 | 773 | function clean(target) { 774 | var this$1 = this; 775 | 776 | var dirty; 777 | try { 778 | each(tealight(target), function (node) { 779 | var id = node.getAttribute('data-sr-id'); 780 | if (id !== null) { 781 | dirty = true; 782 | var element = this$1.store.elements[id]; 783 | if (element.callbackTimer) { 784 | window.clearTimeout(element.callbackTimer.clock); 785 | } 786 | applyStyle(element.node, element.styles.inline.generated); 787 | node.removeAttribute('data-sr-id'); 788 | delete this$1.store.elements[id]; 789 | } 790 | }); 791 | } catch (e) { 792 | return logger.call(this, 'Clean failed.', e.message) 793 | } 794 | 795 | if (dirty) { 796 | try { 797 | rinse.call(this); 798 | } catch (e) { 799 | return logger.call(this, 'Clean failed.', e.message) 800 | } 801 | } 802 | } 803 | 804 | function destroy() { 805 | var this$1 = this; 806 | 807 | /** 808 | * Remove all generated styles and element ids 809 | */ 810 | each(this.store.elements, function (element) { 811 | applyStyle(element.node, element.styles.inline.generated); 812 | element.node.removeAttribute('data-sr-id'); 813 | }); 814 | 815 | /** 816 | * Remove all event listeners. 817 | */ 818 | each(this.store.containers, function (container) { 819 | var target = 820 | container.node === document.documentElement ? window : container.node; 821 | target.removeEventListener('scroll', this$1.delegate); 822 | target.removeEventListener('resize', this$1.delegate); 823 | }); 824 | 825 | /** 826 | * Clear all data from the store 827 | */ 828 | this.store = { 829 | containers: {}, 830 | elements: {}, 831 | history: [], 832 | sequences: {} 833 | }; 834 | } 835 | 836 | function deepAssign(target) { 837 | var sources = [], len = arguments.length - 1; 838 | while ( len-- > 0 ) sources[ len ] = arguments[ len + 1 ]; 839 | 840 | if (isObject(target)) { 841 | each(sources, function (source) { 842 | each(source, function (data, key) { 843 | if (isObject(data)) { 844 | if (!target[key] || !isObject(target[key])) { 845 | target[key] = {}; 846 | } 847 | deepAssign(target[key], data); 848 | } else { 849 | target[key] = data; 850 | } 851 | }); 852 | }); 853 | return target 854 | } else { 855 | throw new TypeError('Target must be an object literal.') 856 | } 857 | } 858 | 859 | function isMobile(agent) { 860 | if ( agent === void 0 ) agent = navigator.userAgent; 861 | 862 | return /Android|iPhone|iPad|iPod/i.test(agent) 863 | } 864 | 865 | var nextUniqueId = (function () { 866 | var uid = 0; 867 | return function () { return uid++; } 868 | })(); 869 | 870 | function initialize() { 871 | var this$1 = this; 872 | 873 | rinse.call(this); 874 | 875 | each(this.store.elements, function (element) { 876 | var styles = [element.styles.inline.generated]; 877 | 878 | if (element.visible) { 879 | styles.push(element.styles.opacity.computed); 880 | styles.push(element.styles.transform.generated.final); 881 | element.revealed = true; 882 | } else { 883 | styles.push(element.styles.opacity.generated); 884 | styles.push(element.styles.transform.generated.initial); 885 | element.revealed = false; 886 | } 887 | 888 | applyStyle(element.node, styles.filter(function (s) { return s !== ''; }).join(' ')); 889 | }); 890 | 891 | each(this.store.containers, function (container) { 892 | var target = 893 | container.node === document.documentElement ? window : container.node; 894 | target.addEventListener('scroll', this$1.delegate); 895 | target.addEventListener('resize', this$1.delegate); 896 | }); 897 | 898 | /** 899 | * Manually invoke delegate once to capture 900 | * element and container dimensions, container 901 | * scroll position, and trigger any valid reveals 902 | */ 903 | this.delegate(); 904 | 905 | /** 906 | * Wipe any existing `setTimeout` now 907 | * that initialization has completed. 908 | */ 909 | this.initTimeout = null; 910 | } 911 | 912 | function animate(element, force) { 913 | if ( force === void 0 ) force = {}; 914 | 915 | var pristine = force.pristine || this.pristine; 916 | var delayed = 917 | element.config.useDelay === 'always' || 918 | (element.config.useDelay === 'onload' && pristine) || 919 | (element.config.useDelay === 'once' && !element.seen); 920 | 921 | var shouldReveal = element.visible && !element.revealed; 922 | var shouldReset = !element.visible && element.revealed && element.config.reset; 923 | 924 | if (force.reveal || shouldReveal) { 925 | return triggerReveal.call(this, element, delayed) 926 | } 927 | 928 | if (force.reset || shouldReset) { 929 | return triggerReset.call(this, element) 930 | } 931 | } 932 | 933 | function triggerReveal(element, delayed) { 934 | var styles = [ 935 | element.styles.inline.generated, 936 | element.styles.opacity.computed, 937 | element.styles.transform.generated.final 938 | ]; 939 | if (delayed) { 940 | styles.push(element.styles.transition.generated.delayed); 941 | } else { 942 | styles.push(element.styles.transition.generated.instant); 943 | } 944 | element.revealed = element.seen = true; 945 | applyStyle(element.node, styles.filter(function (s) { return s !== ''; }).join(' ')); 946 | registerCallbacks.call(this, element, delayed); 947 | } 948 | 949 | function triggerReset(element) { 950 | var styles = [ 951 | element.styles.inline.generated, 952 | element.styles.opacity.generated, 953 | element.styles.transform.generated.initial, 954 | element.styles.transition.generated.instant 955 | ]; 956 | element.revealed = false; 957 | applyStyle(element.node, styles.filter(function (s) { return s !== ''; }).join(' ')); 958 | registerCallbacks.call(this, element); 959 | } 960 | 961 | function registerCallbacks(element, isDelayed) { 962 | var this$1 = this; 963 | 964 | var duration = isDelayed 965 | ? element.config.duration + element.config.delay 966 | : element.config.duration; 967 | 968 | var beforeCallback = element.revealed 969 | ? element.config.beforeReveal 970 | : element.config.beforeReset; 971 | 972 | var afterCallback = element.revealed 973 | ? element.config.afterReveal 974 | : element.config.afterReset; 975 | 976 | var elapsed = 0; 977 | if (element.callbackTimer) { 978 | elapsed = Date.now() - element.callbackTimer.start; 979 | window.clearTimeout(element.callbackTimer.clock); 980 | } 981 | 982 | beforeCallback(element.node); 983 | 984 | element.callbackTimer = { 985 | start: Date.now(), 986 | clock: window.setTimeout(function () { 987 | afterCallback(element.node); 988 | element.callbackTimer = null; 989 | if (element.revealed && !element.config.reset && element.config.cleanup) { 990 | clean.call(this$1, element.node); 991 | } 992 | }, duration - elapsed) 993 | }; 994 | } 995 | 996 | function sequence(element, pristine) { 997 | if ( pristine === void 0 ) pristine = this.pristine; 998 | 999 | /** 1000 | * We first check if the element should reset. 1001 | */ 1002 | if (!element.visible && element.revealed && element.config.reset) { 1003 | return animate.call(this, element, { reset: true }) 1004 | } 1005 | 1006 | var seq = this.store.sequences[element.sequence.id]; 1007 | var i = element.sequence.index; 1008 | 1009 | if (seq) { 1010 | var visible = new SequenceModel(seq, 'visible', this.store); 1011 | var revealed = new SequenceModel(seq, 'revealed', this.store); 1012 | 1013 | seq.models = { visible: visible, revealed: revealed }; 1014 | 1015 | /** 1016 | * If the sequence has no revealed members, 1017 | * then we reveal the first visible element 1018 | * within that sequence. 1019 | * 1020 | * The sequence then cues a recursive call 1021 | * in both directions. 1022 | */ 1023 | if (!revealed.body.length) { 1024 | var nextId = seq.members[visible.body[0]]; 1025 | var nextElement = this.store.elements[nextId]; 1026 | 1027 | if (nextElement) { 1028 | cue.call(this, seq, visible.body[0], -1, pristine); 1029 | cue.call(this, seq, visible.body[0], +1, pristine); 1030 | return animate.call(this, nextElement, { reveal: true, pristine: pristine }) 1031 | } 1032 | } 1033 | 1034 | /** 1035 | * If our element isn’t resetting, we check the 1036 | * element sequence index against the head, and 1037 | * then the foot of the sequence. 1038 | */ 1039 | if ( 1040 | !seq.blocked.head && 1041 | i === [].concat( revealed.head ).pop() && 1042 | i >= [].concat( visible.body ).shift() 1043 | ) { 1044 | cue.call(this, seq, i, -1, pristine); 1045 | return animate.call(this, element, { reveal: true, pristine: pristine }) 1046 | } 1047 | 1048 | if ( 1049 | !seq.blocked.foot && 1050 | i === [].concat( revealed.foot ).shift() && 1051 | i <= [].concat( visible.body ).pop() 1052 | ) { 1053 | cue.call(this, seq, i, +1, pristine); 1054 | return animate.call(this, element, { reveal: true, pristine: pristine }) 1055 | } 1056 | } 1057 | } 1058 | 1059 | function Sequence(interval) { 1060 | var i = Math.abs(interval); 1061 | if (!isNaN(i)) { 1062 | this.id = nextUniqueId(); 1063 | this.interval = Math.max(i, 16); 1064 | this.members = []; 1065 | this.models = {}; 1066 | this.blocked = { 1067 | head: false, 1068 | foot: false 1069 | }; 1070 | } else { 1071 | throw new RangeError('Invalid sequence interval.') 1072 | } 1073 | } 1074 | 1075 | function SequenceModel(seq, prop, store) { 1076 | var this$1 = this; 1077 | 1078 | this.head = []; 1079 | this.body = []; 1080 | this.foot = []; 1081 | 1082 | each(seq.members, function (id, index) { 1083 | var element = store.elements[id]; 1084 | if (element && element[prop]) { 1085 | this$1.body.push(index); 1086 | } 1087 | }); 1088 | 1089 | if (this.body.length) { 1090 | each(seq.members, function (id, index) { 1091 | var element = store.elements[id]; 1092 | if (element && !element[prop]) { 1093 | if (index < this$1.body[0]) { 1094 | this$1.head.push(index); 1095 | } else { 1096 | this$1.foot.push(index); 1097 | } 1098 | } 1099 | }); 1100 | } 1101 | } 1102 | 1103 | function cue(seq, i, direction, pristine) { 1104 | var this$1 = this; 1105 | 1106 | var blocked = ['head', null, 'foot'][1 + direction]; 1107 | var nextId = seq.members[i + direction]; 1108 | var nextElement = this.store.elements[nextId]; 1109 | 1110 | seq.blocked[blocked] = true; 1111 | 1112 | setTimeout(function () { 1113 | seq.blocked[blocked] = false; 1114 | if (nextElement) { 1115 | sequence.call(this$1, nextElement, pristine); 1116 | } 1117 | }, seq.interval); 1118 | } 1119 | 1120 | function reveal(target, options, syncing) { 1121 | var this$1 = this; 1122 | if ( options === void 0 ) options = {}; 1123 | if ( syncing === void 0 ) syncing = false; 1124 | 1125 | var containerBuffer = []; 1126 | var sequence$1; 1127 | var interval = options.interval || defaults.interval; 1128 | 1129 | try { 1130 | if (interval) { 1131 | sequence$1 = new Sequence(interval); 1132 | } 1133 | 1134 | var nodes = tealight(target); 1135 | if (!nodes.length) { 1136 | throw new Error('Invalid reveal target.') 1137 | } 1138 | 1139 | var elements = nodes.reduce(function (elementBuffer, elementNode) { 1140 | var element = {}; 1141 | var existingId = elementNode.getAttribute('data-sr-id'); 1142 | 1143 | if (existingId) { 1144 | deepAssign(element, this$1.store.elements[existingId]); 1145 | 1146 | /** 1147 | * In order to prevent previously generated styles 1148 | * from throwing off the new styles, the style tag 1149 | * has to be reverted to its pre-reveal state. 1150 | */ 1151 | applyStyle(element.node, element.styles.inline.computed); 1152 | } else { 1153 | element.id = nextUniqueId(); 1154 | element.node = elementNode; 1155 | element.seen = false; 1156 | element.revealed = false; 1157 | element.visible = false; 1158 | } 1159 | 1160 | var config = deepAssign({}, element.config || this$1.defaults, options); 1161 | 1162 | if ((!config.mobile && isMobile()) || (!config.desktop && !isMobile())) { 1163 | if (existingId) { 1164 | clean.call(this$1, element); 1165 | } 1166 | return elementBuffer // skip elements that are disabled 1167 | } 1168 | 1169 | var containerNode = tealight(config.container)[0]; 1170 | if (!containerNode) { 1171 | throw new Error('Invalid container.') 1172 | } 1173 | if (!containerNode.contains(elementNode)) { 1174 | return elementBuffer // skip elements found outside the container 1175 | } 1176 | 1177 | var containerId; 1178 | { 1179 | containerId = getContainerId( 1180 | containerNode, 1181 | containerBuffer, 1182 | this$1.store.containers 1183 | ); 1184 | if (containerId === null) { 1185 | containerId = nextUniqueId(); 1186 | containerBuffer.push({ id: containerId, node: containerNode }); 1187 | } 1188 | } 1189 | 1190 | element.config = config; 1191 | element.containerId = containerId; 1192 | element.styles = style(element); 1193 | 1194 | if (sequence$1) { 1195 | element.sequence = { 1196 | id: sequence$1.id, 1197 | index: sequence$1.members.length 1198 | }; 1199 | sequence$1.members.push(element.id); 1200 | } 1201 | 1202 | elementBuffer.push(element); 1203 | return elementBuffer 1204 | }, []); 1205 | 1206 | /** 1207 | * Modifying the DOM via setAttribute needs to be handled 1208 | * separately from reading computed styles in the map above 1209 | * for the browser to batch DOM changes (limiting reflows) 1210 | */ 1211 | each(elements, function (element) { 1212 | this$1.store.elements[element.id] = element; 1213 | element.node.setAttribute('data-sr-id', element.id); 1214 | }); 1215 | } catch (e) { 1216 | return logger.call(this, 'Reveal failed.', e.message) 1217 | } 1218 | 1219 | /** 1220 | * Now that element set-up is complete... 1221 | * Let’s commit any container and sequence data we have to the store. 1222 | */ 1223 | each(containerBuffer, function (container) { 1224 | this$1.store.containers[container.id] = { 1225 | id: container.id, 1226 | node: container.node 1227 | }; 1228 | }); 1229 | if (sequence$1) { 1230 | this.store.sequences[sequence$1.id] = sequence$1; 1231 | } 1232 | 1233 | /** 1234 | * If reveal wasn't invoked by sync, we want to 1235 | * make sure to add this call to the history. 1236 | */ 1237 | if (syncing !== true) { 1238 | this.store.history.push({ target: target, options: options }); 1239 | 1240 | /** 1241 | * Push initialization to the event queue, giving 1242 | * multiple reveal calls time to be interpreted. 1243 | */ 1244 | if (this.initTimeout) { 1245 | window.clearTimeout(this.initTimeout); 1246 | } 1247 | this.initTimeout = window.setTimeout(initialize.bind(this), 0); 1248 | } 1249 | } 1250 | 1251 | function getContainerId(node) { 1252 | var collections = [], len = arguments.length - 1; 1253 | while ( len-- > 0 ) collections[ len ] = arguments[ len + 1 ]; 1254 | 1255 | var id = null; 1256 | each(collections, function (collection) { 1257 | each(collection, function (container) { 1258 | if (id === null && container.node === node) { 1259 | id = container.id; 1260 | } 1261 | }); 1262 | }); 1263 | return id 1264 | } 1265 | 1266 | /** 1267 | * Re-runs the reveal method for each record stored in history, 1268 | * for capturing new content asynchronously loaded into the DOM. 1269 | */ 1270 | function sync() { 1271 | var this$1 = this; 1272 | 1273 | each(this.store.history, function (record) { 1274 | reveal.call(this$1, record.target, record.options, true); 1275 | }); 1276 | 1277 | initialize.call(this); 1278 | } 1279 | 1280 | var polyfill = function (x) { return (x > 0) - (x < 0) || +x; }; 1281 | var mathSign = Math.sign || polyfill; 1282 | 1283 | /*! @license miniraf v1.0.1 1284 | 1285 | Copyright 2018 Fisssion LLC. 1286 | 1287 | Permission is hereby granted, free of charge, to any person obtaining a copy 1288 | of this software and associated documentation files (the "Software"), to deal 1289 | in the Software without restriction, including without limitation the rights 1290 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 1291 | copies of the Software, and to permit persons to whom the Software is 1292 | furnished to do so, subject to the following conditions: 1293 | 1294 | The above copyright notice and this permission notice shall be included in all 1295 | copies or substantial portions of the Software. 1296 | 1297 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1298 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 1299 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 1300 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 1301 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 1302 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 1303 | SOFTWARE. 1304 | 1305 | */ 1306 | var polyfill$1 = (function () { 1307 | var clock = Date.now(); 1308 | 1309 | return function (callback) { 1310 | var currentTime = Date.now(); 1311 | if (currentTime - clock > 16) { 1312 | clock = currentTime; 1313 | callback(currentTime); 1314 | } else { 1315 | setTimeout(function () { return polyfill$1(callback); }, 0); 1316 | } 1317 | } 1318 | })(); 1319 | 1320 | var miniraf = window.requestAnimationFrame || 1321 | window.webkitRequestAnimationFrame || 1322 | window.mozRequestAnimationFrame || 1323 | polyfill$1; 1324 | 1325 | function getGeometry(target, isContainer) { 1326 | /** 1327 | * We want to ignore padding and scrollbars for container elements. 1328 | * More information here: https://goo.gl/vOZpbz 1329 | */ 1330 | var height = isContainer ? target.node.clientHeight : target.node.offsetHeight; 1331 | var width = isContainer ? target.node.clientWidth : target.node.offsetWidth; 1332 | 1333 | var offsetTop = 0; 1334 | var offsetLeft = 0; 1335 | var node = target.node; 1336 | 1337 | do { 1338 | if (!isNaN(node.offsetTop)) { 1339 | offsetTop += node.offsetTop; 1340 | } 1341 | if (!isNaN(node.offsetLeft)) { 1342 | offsetLeft += node.offsetLeft; 1343 | } 1344 | node = node.offsetParent; 1345 | } while (node) 1346 | 1347 | return { 1348 | bounds: { 1349 | top: offsetTop, 1350 | right: offsetLeft + width, 1351 | bottom: offsetTop + height, 1352 | left: offsetLeft 1353 | }, 1354 | height: height, 1355 | width: width 1356 | } 1357 | } 1358 | 1359 | function getScrolled(container) { 1360 | var top, left; 1361 | if (container.node === document.documentElement) { 1362 | top = window.pageYOffset; 1363 | left = window.pageXOffset; 1364 | } else { 1365 | top = container.node.scrollTop; 1366 | left = container.node.scrollLeft; 1367 | } 1368 | return { top: top, left: left } 1369 | } 1370 | 1371 | function isElementVisible(element) { 1372 | if ( element === void 0 ) element = {}; 1373 | 1374 | var container = this.store.containers[element.containerId]; 1375 | if (!container) { return } 1376 | 1377 | var viewFactor = Math.max(0, Math.min(1, element.config.viewFactor)); 1378 | var viewOffset = element.config.viewOffset; 1379 | 1380 | var elementBounds = { 1381 | top: element.geometry.bounds.top + element.geometry.height * viewFactor, 1382 | right: element.geometry.bounds.right - element.geometry.width * viewFactor, 1383 | bottom: element.geometry.bounds.bottom - element.geometry.height * viewFactor, 1384 | left: element.geometry.bounds.left + element.geometry.width * viewFactor 1385 | }; 1386 | 1387 | var containerBounds = { 1388 | top: container.geometry.bounds.top + container.scroll.top + viewOffset.top, 1389 | right: container.geometry.bounds.right + container.scroll.left - viewOffset.right, 1390 | bottom: 1391 | container.geometry.bounds.bottom + container.scroll.top - viewOffset.bottom, 1392 | left: container.geometry.bounds.left + container.scroll.left + viewOffset.left 1393 | }; 1394 | 1395 | return ( 1396 | (elementBounds.top < containerBounds.bottom && 1397 | elementBounds.right > containerBounds.left && 1398 | elementBounds.bottom > containerBounds.top && 1399 | elementBounds.left < containerBounds.right) || 1400 | element.styles.position === 'fixed' 1401 | ) 1402 | } 1403 | 1404 | function delegate( 1405 | event, 1406 | elements 1407 | ) { 1408 | var this$1 = this; 1409 | if ( event === void 0 ) event = { type: 'init' }; 1410 | if ( elements === void 0 ) elements = this.store.elements; 1411 | 1412 | miniraf(function () { 1413 | var stale = event.type === 'init' || event.type === 'resize'; 1414 | 1415 | each(this$1.store.containers, function (container) { 1416 | if (stale) { 1417 | container.geometry = getGeometry.call(this$1, container, true); 1418 | } 1419 | var scroll = getScrolled.call(this$1, container); 1420 | if (container.scroll) { 1421 | container.direction = { 1422 | x: mathSign(scroll.left - container.scroll.left), 1423 | y: mathSign(scroll.top - container.scroll.top) 1424 | }; 1425 | } 1426 | container.scroll = scroll; 1427 | }); 1428 | 1429 | /** 1430 | * Due to how the sequencer is implemented, it’s 1431 | * important that we update the state of all 1432 | * elements, before any animation logic is 1433 | * evaluated (in the second loop below). 1434 | */ 1435 | each(elements, function (element) { 1436 | if (stale || element.geometry === undefined) { 1437 | element.geometry = getGeometry.call(this$1, element); 1438 | } 1439 | element.visible = isElementVisible.call(this$1, element); 1440 | }); 1441 | 1442 | each(elements, function (element) { 1443 | if (element.sequence) { 1444 | sequence.call(this$1, element); 1445 | } else { 1446 | animate.call(this$1, element); 1447 | } 1448 | }); 1449 | 1450 | this$1.pristine = false; 1451 | }); 1452 | } 1453 | 1454 | function isTransformSupported() { 1455 | var style = document.documentElement.style; 1456 | return 'transform' in style || 'WebkitTransform' in style 1457 | } 1458 | 1459 | function isTransitionSupported() { 1460 | var style = document.documentElement.style; 1461 | return 'transition' in style || 'WebkitTransition' in style 1462 | } 1463 | 1464 | var version = "4.0.9"; 1465 | 1466 | var boundDelegate; 1467 | var boundDestroy; 1468 | var boundReveal; 1469 | var boundClean; 1470 | var boundSync; 1471 | var config; 1472 | var debug; 1473 | var instance; 1474 | 1475 | function ScrollReveal(options) { 1476 | if ( options === void 0 ) options = {}; 1477 | 1478 | var invokedWithoutNew = 1479 | typeof this === 'undefined' || 1480 | Object.getPrototypeOf(this) !== ScrollReveal.prototype; 1481 | 1482 | if (invokedWithoutNew) { 1483 | return new ScrollReveal(options) 1484 | } 1485 | 1486 | if (!ScrollReveal.isSupported()) { 1487 | logger.call(this, 'Instantiation failed.', 'This browser is not supported.'); 1488 | return mount.failure() 1489 | } 1490 | 1491 | var buffer; 1492 | try { 1493 | buffer = config 1494 | ? deepAssign({}, config, options) 1495 | : deepAssign({}, defaults, options); 1496 | } catch (e) { 1497 | logger.call(this, 'Invalid configuration.', e.message); 1498 | return mount.failure() 1499 | } 1500 | 1501 | try { 1502 | var container = tealight(buffer.container)[0]; 1503 | if (!container) { 1504 | throw new Error('Invalid container.') 1505 | } 1506 | } catch (e) { 1507 | logger.call(this, e.message); 1508 | return mount.failure() 1509 | } 1510 | 1511 | config = buffer; 1512 | 1513 | if ((!config.mobile && isMobile()) || (!config.desktop && !isMobile())) { 1514 | logger.call( 1515 | this, 1516 | 'This device is disabled.', 1517 | ("desktop: " + (config.desktop)), 1518 | ("mobile: " + (config.mobile)) 1519 | ); 1520 | return mount.failure() 1521 | } 1522 | 1523 | mount.success(); 1524 | 1525 | this.store = { 1526 | containers: {}, 1527 | elements: {}, 1528 | history: [], 1529 | sequences: {} 1530 | }; 1531 | 1532 | this.pristine = true; 1533 | 1534 | boundDelegate = boundDelegate || delegate.bind(this); 1535 | boundDestroy = boundDestroy || destroy.bind(this); 1536 | boundReveal = boundReveal || reveal.bind(this); 1537 | boundClean = boundClean || clean.bind(this); 1538 | boundSync = boundSync || sync.bind(this); 1539 | 1540 | Object.defineProperty(this, 'delegate', { get: function () { return boundDelegate; } }); 1541 | Object.defineProperty(this, 'destroy', { get: function () { return boundDestroy; } }); 1542 | Object.defineProperty(this, 'reveal', { get: function () { return boundReveal; } }); 1543 | Object.defineProperty(this, 'clean', { get: function () { return boundClean; } }); 1544 | Object.defineProperty(this, 'sync', { get: function () { return boundSync; } }); 1545 | 1546 | Object.defineProperty(this, 'defaults', { get: function () { return config; } }); 1547 | Object.defineProperty(this, 'version', { get: function () { return version; } }); 1548 | Object.defineProperty(this, 'noop', { get: function () { return false; } }); 1549 | 1550 | return instance ? instance : (instance = this) 1551 | } 1552 | 1553 | ScrollReveal.isSupported = function () { return isTransformSupported() && isTransitionSupported(); }; 1554 | 1555 | Object.defineProperty(ScrollReveal, 'debug', { 1556 | get: function () { return debug || false; }, 1557 | set: function (value) { return (debug = typeof value === 'boolean' ? value : debug); } 1558 | }); 1559 | 1560 | ScrollReveal(); 1561 | 1562 | return ScrollReveal; 1563 | 1564 | })); 1565 | -------------------------------------------------------------------------------- /dist/scrollreveal.min.js: -------------------------------------------------------------------------------- 1 | /*! @license ScrollReveal v4.0.9 2 | 3 | Copyright 2021 Fisssion LLC. 4 | 5 | Licensed under the GNU General Public License 3.0 for 6 | compatible open source projects and non-commercial use. 7 | 8 | For commercial sites, themes, projects, and applications, 9 | keep your source code private/proprietary by purchasing 10 | a commercial license from https://scrollrevealjs.org/ 11 | */ 12 | var ScrollReveal=function(){"use strict";var r={delay:0,distance:"0",duration:600,easing:"cubic-bezier(0.5, 0, 0, 1)",interval:0,opacity:0,origin:"bottom",rotate:{x:0,y:0,z:0},scale:1,cleanup:!1,container:document.documentElement,desktop:!0,mobile:!0,reset:!1,useDelay:"always",viewFactor:0,viewOffset:{top:0,right:0,bottom:0,left:0},afterReset:function(){},afterReveal:function(){},beforeReset:function(){},beforeReveal:function(){}};var n={success:function(){document.documentElement.classList.add("sr"),document.body?document.body.style.height="100%":document.addEventListener("DOMContentLoaded",function(){document.body.style.height="100%"})},failure:function(){return document.documentElement.classList.remove("sr"),{clean:function(){},destroy:function(){},reveal:function(){},sync:function(){},get noop(){return!0}}}};function o(e){return"object"==typeof window.Node?e instanceof window.Node:null!==e&&"object"==typeof e&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName}function u(e,t){if(void 0===t&&(t=document),e instanceof Array)return e.filter(o);if(o(e))return[e];if(n=e,i=Object.prototype.toString.call(n),"object"==typeof window.NodeList?n instanceof window.NodeList:null!==n&&"object"==typeof n&&"number"==typeof n.length&&/^\[object (HTMLCollection|NodeList|Object)\]$/.test(i)&&(0===n.length||o(n[0])))return Array.prototype.slice.call(e);var n,i;if("string"==typeof e)try{var r=t.querySelectorAll(e);return Array.prototype.slice.call(r)}catch(e){return[]}return[]}function s(e){return null!==e&&e instanceof Object&&(e.constructor===Object||"[object Object]"===Object.prototype.toString.call(e))}function f(n,i){if(s(n))return Object.keys(n).forEach(function(e){return i(n[e],e,n)});if(n instanceof Array)return n.forEach(function(e,t){return i(e,t,n)});throw new TypeError("Expected either an array or object literal.")}function h(e){for(var t=[],n=arguments.length-1;0<n--;)t[n]=arguments[n+1];if(this.constructor.debug&&console){var i="%cScrollReveal: "+e;t.forEach(function(e){return i+="\n — "+e}),console.log(i,"color: #ea654b;")}}function t(){var n=this,i={active:[],stale:[]},t={active:[],stale:[]},r={active:[],stale:[]};try{f(u("[data-sr-id]"),function(e){var t=parseInt(e.getAttribute("data-sr-id"));i.active.push(t)})}catch(e){throw e}f(this.store.elements,function(e){-1===i.active.indexOf(e.id)&&i.stale.push(e.id)}),f(i.stale,function(e){return delete n.store.elements[e]}),f(this.store.elements,function(e){-1===r.active.indexOf(e.containerId)&&r.active.push(e.containerId),e.hasOwnProperty("sequence")&&-1===t.active.indexOf(e.sequence.id)&&t.active.push(e.sequence.id)}),f(this.store.containers,function(e){-1===r.active.indexOf(e.id)&&r.stale.push(e.id)}),f(r.stale,function(e){var t=n.store.containers[e].node;t.removeEventListener("scroll",n.delegate),t.removeEventListener("resize",n.delegate),delete n.store.containers[e]}),f(this.store.sequences,function(e){-1===t.active.indexOf(e.id)&&t.stale.push(e.id)}),f(t.stale,function(e){return delete n.store.sequences[e]})}function N(e){if(e.constructor!==Array)throw new TypeError("Expected array.");if(16===e.length)return e;if(6!==e.length)throw new RangeError("Expected array with either 6 or 16 values.");var t=z();return t[0]=e[0],t[1]=e[1],t[4]=e[2],t[5]=e[3],t[12]=e[4],t[13]=e[5],t}function z(){for(var e=[],t=0;t<16;t++)t%5==0?e.push(1):e.push(0);return e}function F(e,t){for(var n=N(e),i=N(t),r=[],o=0;o<4;o++)for(var s=[n[o],n[o+4],n[o+8],n[o+12]],a=0;a<4;a++){var c=4*a,l=[i[c],i[c+1],i[c+2],i[c+3]],d=s[0]*l[0]+s[1]*l[1]+s[2]*l[2]+s[3]*l[3];r[o+c]=d}return r}function D(e,t){var n=z();return n[0]=e,n[5]="number"==typeof t?t:e,n}var S=function(){var n={},i=document.documentElement.style;function e(e,t){if(void 0===t&&(t=i),e&&"string"==typeof e){if(n[e])return n[e];if("string"==typeof t[e])return n[e]=e;if("string"==typeof t["-webkit-"+e])return n[e]="-webkit-"+e;throw new RangeError('Unable to find "'+e+'" style property.')}throw new TypeError("Expected a string.")}return e.clearCache=function(){return n={}},e}();function p(e){var t=window.getComputedStyle(e.node),n=t.position,i=e.config,r={},o=(e.node.getAttribute("style")||"").match(/[\w-]+\s*:\s*[^;]+\s*/gi)||[];r.computed=o?o.map(function(e){return e.trim()}).join("; ")+";":"",r.generated=o.some(function(e){return e.match(/visibility\s?:\s?visible/i)})?r.computed:o.concat(["visibility: visible"]).map(function(e){return e.trim()}).join("; ")+";";var s,a,c,l,d,u,f,h,p,m,y,v,g,b=parseFloat(t.opacity),w=isNaN(parseFloat(i.opacity))?parseFloat(t.opacity):parseFloat(i.opacity),E={computed:b!==w?"opacity: "+b+";":"",generated:b!==w?"opacity: "+w+";":""},j=[];if(parseFloat(i.distance)){var T="top"===i.origin||"bottom"===i.origin?"Y":"X",k=i.distance;"top"!==i.origin&&"left"!==i.origin||(k=/^-/.test(k)?k.substr(1):"-"+k);var O=k.match(/(^-?\d+\.?\d?)|(em$|px$|%$)/g),x=O[0];switch(O[1]){case"em":k=parseInt(t.fontSize)*x;break;case"px":k=x;break;case"%":k="Y"===T?e.node.getBoundingClientRect().height*x/100:e.node.getBoundingClientRect().width*x/100;break;default:throw new RangeError("Unrecognized or missing distance unit.")}"Y"===T?j.push((c=k,(l=z())[13]=c,l)):j.push((s=k,(a=z())[12]=s,a))}i.rotate.x&&j.push((d=i.rotate.x,u=Math.PI/180*d,(f=z())[5]=f[10]=Math.cos(u),f[6]=f[9]=Math.sin(u),f[9]*=-1,f)),i.rotate.y&&j.push((h=i.rotate.y,p=Math.PI/180*h,(m=z())[0]=m[10]=Math.cos(p),m[2]=m[8]=Math.sin(p),m[2]*=-1,m)),i.rotate.z&&j.push((y=i.rotate.z,v=Math.PI/180*y,(g=z())[0]=g[5]=Math.cos(v),g[1]=g[4]=Math.sin(v),g[4]*=-1,g)),1!==i.scale&&(0===i.scale?j.push(D(2e-4)):j.push(D(i.scale)));var R={};if(j.length){R.property=S("transform"),R.computed={raw:t[R.property],matrix:function(e){if("string"==typeof e){var t=e.match(/matrix(3d)?\(([^)]+)\)/);if(t)return N(t[2].split(", ").map(parseFloat))}return z()}(t[R.property])},j.unshift(R.computed.matrix);var q=j.reduce(F);R.generated={initial:R.property+": matrix3d("+q.join(", ")+");",final:R.property+": matrix3d("+R.computed.matrix.join(", ")+");"}}else R.generated={initial:"",final:""};var A={};if(E.generated||R.generated.initial){A.property=S("transition"),A.computed=t[A.property],A.fragments=[];var P=i.delay,L=i.duration,M=i.easing;E.generated&&A.fragments.push({delayed:"opacity "+L/1e3+"s "+M+" "+P/1e3+"s",instant:"opacity "+L/1e3+"s "+M+" 0s"}),R.generated.initial&&A.fragments.push({delayed:R.property+" "+L/1e3+"s "+M+" "+P/1e3+"s",instant:R.property+" "+L/1e3+"s "+M+" 0s"}),A.computed&&!A.computed.match(/all 0s|none 0s/)&&A.fragments.unshift({delayed:A.computed,instant:A.computed});var I=A.fragments.reduce(function(e,t,n){return e.delayed+=0===n?t.delayed:", "+t.delayed,e.instant+=0===n?t.instant:", "+t.instant,e},{delayed:"",instant:""});A.generated={delayed:A.property+": "+I.delayed+";",instant:A.property+": "+I.instant+";"}}else A.generated={delayed:"",instant:""};return{inline:r,opacity:E,position:n,transform:R,transition:A}}function m(r,e){e.split(";").forEach(function(e){var t=e.split(":"),n=t[0],i=t.slice(1);n&&i&&(r.style[n.trim()]=i.join(":"))})}function y(e){var i,r=this;try{f(u(e),function(e){var t=e.getAttribute("data-sr-id");if(null!==t){i=!0;var n=r.store.elements[t];n.callbackTimer&&window.clearTimeout(n.callbackTimer.clock),m(n.node,n.styles.inline.generated),e.removeAttribute("data-sr-id"),delete r.store.elements[t]}})}catch(e){return h.call(this,"Clean failed.",e.message)}if(i)try{t.call(this)}catch(e){return h.call(this,"Clean failed.",e.message)}}function v(n){for(var e=[],t=arguments.length-1;0<t--;)e[t]=arguments[t+1];if(s(n))return f(e,function(e){f(e,function(e,t){s(e)?(n[t]&&s(n[t])||(n[t]={}),v(n[t],e)):n[t]=e})}),n;throw new TypeError("Target must be an object literal.")}function g(e){return void 0===e&&(e=navigator.userAgent),/Android|iPhone|iPad|iPod/i.test(e)}var e,b=(e=0,function(){return e++});function w(){var n=this;t.call(this),f(this.store.elements,function(e){var t=[e.styles.inline.generated];e.visible?(t.push(e.styles.opacity.computed),t.push(e.styles.transform.generated.final),e.revealed=!0):(t.push(e.styles.opacity.generated),t.push(e.styles.transform.generated.initial),e.revealed=!1),m(e.node,t.filter(function(e){return""!==e}).join(" "))}),f(this.store.containers,function(e){var t=e.node===document.documentElement?window:e.node;t.addEventListener("scroll",n.delegate),t.addEventListener("resize",n.delegate)}),this.delegate(),this.initTimeout=null}function c(e,t){void 0===t&&(t={});var n=t.pristine||this.pristine,i="always"===e.config.useDelay||"onload"===e.config.useDelay&&n||"once"===e.config.useDelay&&!e.seen,r=e.visible&&!e.revealed,o=!e.visible&&e.revealed&&e.config.reset;return t.reveal||r?function(e,t){var n=[e.styles.inline.generated,e.styles.opacity.computed,e.styles.transform.generated.final];t?n.push(e.styles.transition.generated.delayed):n.push(e.styles.transition.generated.instant);e.revealed=e.seen=!0,m(e.node,n.filter(function(e){return""!==e}).join(" ")),a.call(this,e,t)}.call(this,e,i):t.reset||o?function(e){var t=[e.styles.inline.generated,e.styles.opacity.generated,e.styles.transform.generated.initial,e.styles.transition.generated.instant];e.revealed=!1,m(e.node,t.filter(function(e){return""!==e}).join(" ")),a.call(this,e)}.call(this,e):void 0}function a(e,t){var n=this,i=t?e.config.duration+e.config.delay:e.config.duration,r=e.revealed?e.config.beforeReveal:e.config.beforeReset,o=e.revealed?e.config.afterReveal:e.config.afterReset,s=0;e.callbackTimer&&(s=Date.now()-e.callbackTimer.start,window.clearTimeout(e.callbackTimer.clock)),r(e.node),e.callbackTimer={start:Date.now(),clock:window.setTimeout(function(){o(e.node),e.callbackTimer=null,e.revealed&&!e.config.reset&&e.config.cleanup&&y.call(n,e.node)},i-s)}}function l(e,t){if(void 0===t&&(t=this.pristine),!e.visible&&e.revealed&&e.config.reset)return c.call(this,e,{reset:!0});var n=this.store.sequences[e.sequence.id],i=e.sequence.index;if(n){var r=new d(n,"visible",this.store),o=new d(n,"revealed",this.store);if(n.models={visible:r,revealed:o},!o.body.length){var s=n.members[r.body[0]],a=this.store.elements[s];if(a)return j.call(this,n,r.body[0],-1,t),j.call(this,n,r.body[0],1,t),c.call(this,a,{reveal:!0,pristine:t})}if(!n.blocked.head&&i===[].concat(o.head).pop()&&i>=[].concat(r.body).shift())return j.call(this,n,i,-1,t),c.call(this,e,{reveal:!0,pristine:t});if(!n.blocked.foot&&i===[].concat(o.foot).shift()&&i<=[].concat(r.body).pop())return j.call(this,n,i,1,t),c.call(this,e,{reveal:!0,pristine:t})}}function E(e){var t=Math.abs(e);if(isNaN(t))throw new RangeError("Invalid sequence interval.");this.id=b(),this.interval=Math.max(t,16),this.members=[],this.models={},this.blocked={head:!1,foot:!1}}function d(e,i,r){var o=this;this.head=[],this.body=[],this.foot=[],f(e.members,function(e,t){var n=r.elements[e];n&&n[i]&&o.body.push(t)}),this.body.length&&f(e.members,function(e,t){var n=r.elements[e];n&&!n[i]&&(t<o.body[0]?o.head.push(t):o.foot.push(t))})}function j(e,t,n,i){var r=this,o=["head",null,"foot"][1+n],s=e.members[t+n],a=this.store.elements[s];e.blocked[o]=!0,setTimeout(function(){e.blocked[o]=!1,a&&l.call(r,a,i)},e.interval)}function i(e,a,t){var c=this;void 0===a&&(a={}),void 0===t&&(t=!1);var l,d=[],n=a.interval||r.interval;try{n&&(l=new E(n));var i=u(e);if(!i.length)throw new Error("Invalid reveal target.");f(i.reduce(function(e,t){var n={},i=t.getAttribute("data-sr-id");i?(v(n,c.store.elements[i]),m(n.node,n.styles.inline.computed)):(n.id=b(),n.node=t,n.seen=!1,n.revealed=!1,n.visible=!1);var r=v({},n.config||c.defaults,a);if(!r.mobile&&g()||!r.desktop&&!g())return i&&y.call(c,n),e;var o,s=u(r.container)[0];if(!s)throw new Error("Invalid container.");return s.contains(t)&&(null===(o=function(t){var e=[],n=arguments.length-1;for(;0<n--;)e[n]=arguments[n+1];var i=null;return f(e,function(e){f(e,function(e){null===i&&e.node===t&&(i=e.id)})}),i}(s,d,c.store.containers))&&(o=b(),d.push({id:o,node:s})),n.config=r,n.containerId=o,n.styles=p(n),l&&(n.sequence={id:l.id,index:l.members.length},l.members.push(n.id)),e.push(n)),e},[]),function(e){(c.store.elements[e.id]=e).node.setAttribute("data-sr-id",e.id)})}catch(e){return h.call(this,"Reveal failed.",e.message)}f(d,function(e){c.store.containers[e.id]={id:e.id,node:e.node}}),l&&(this.store.sequences[l.id]=l),!0!==t&&(this.store.history.push({target:e,options:a}),this.initTimeout&&window.clearTimeout(this.initTimeout),this.initTimeout=window.setTimeout(w.bind(this),0))}var T,k=Math.sign||function(e){return(0<e)-(e<0)||+e},O=(T=Date.now(),function(e){var t=Date.now();16<t-T?e(T=t):setTimeout(function(){return O(e)},0)}),x=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||O;function R(e,t){for(var n=t?e.node.clientHeight:e.node.offsetHeight,i=t?e.node.clientWidth:e.node.offsetWidth,r=0,o=0,s=e.node;isNaN(s.offsetTop)||(r+=s.offsetTop),isNaN(s.offsetLeft)||(o+=s.offsetLeft),s=s.offsetParent;);return{bounds:{top:r,right:o+i,bottom:r+n,left:o},height:n,width:i}}function q(e,t){var i=this;void 0===e&&(e={type:"init"}),void 0===t&&(t=this.store.elements),x(function(){var n="init"===e.type||"resize"===e.type;f(i.store.containers,function(e){n&&(e.geometry=R.call(i,e,!0));var t=function(e){var t,n;return n=e.node===document.documentElement?(t=window.pageYOffset,window.pageXOffset):(t=e.node.scrollTop,e.node.scrollLeft),{top:t,left:n}}.call(i,e);e.scroll&&(e.direction={x:k(t.left-e.scroll.left),y:k(t.top-e.scroll.top)}),e.scroll=t}),f(t,function(e){(n||void 0===e.geometry)&&(e.geometry=R.call(i,e)),e.visible=function(e){void 0===e&&(e={});var t=this.store.containers[e.containerId];if(t){var n=Math.max(0,Math.min(1,e.config.viewFactor)),i=e.config.viewOffset,r=e.geometry.bounds.top+e.geometry.height*n,o=e.geometry.bounds.right-e.geometry.width*n,s=e.geometry.bounds.bottom-e.geometry.height*n,a=e.geometry.bounds.left+e.geometry.width*n,c=t.geometry.bounds.top+t.scroll.top+i.top,l=t.geometry.bounds.right+t.scroll.left-i.right,d=t.geometry.bounds.bottom+t.scroll.top-i.bottom,u=t.geometry.bounds.left+t.scroll.left+i.left;return r<d&&u<o&&c<s&&a<l||"fixed"===e.styles.position}}.call(i,e)}),f(t,function(e){e.sequence?l.call(i,e):c.call(i,e)}),i.pristine=!1})}var A,P,L,M,I,C,W,Y,$="4.0.9";function H(e){var t;if(void 0===e&&(e={}),void 0===this||Object.getPrototypeOf(this)!==H.prototype)return new H(e);if(!H.isSupported())return h.call(this,"Instantiation failed.","This browser is not supported."),n.failure();try{t=v({},C||r,e)}catch(e){return h.call(this,"Invalid configuration.",e.message),n.failure()}try{if(!u(t.container)[0])throw new Error("Invalid container.")}catch(e){return h.call(this,e.message),n.failure()}return!(C=t).mobile&&g()||!C.desktop&&!g()?(h.call(this,"This device is disabled.","desktop: "+C.desktop,"mobile: "+C.mobile),n.failure()):(n.success(),this.store={containers:{},elements:{},history:[],sequences:{}},this.pristine=!0,A=A||q.bind(this),P=P||function(){var n=this;f(this.store.elements,function(e){m(e.node,e.styles.inline.generated),e.node.removeAttribute("data-sr-id")}),f(this.store.containers,function(e){var t=e.node===document.documentElement?window:e.node;t.removeEventListener("scroll",n.delegate),t.removeEventListener("resize",n.delegate)}),this.store={containers:{},elements:{},history:[],sequences:{}}}.bind(this),L=L||i.bind(this),M=M||y.bind(this),I=I||function(){var t=this;f(this.store.history,function(e){i.call(t,e.target,e.options,!0)}),w.call(this)}.bind(this),Object.defineProperty(this,"delegate",{get:function(){return A}}),Object.defineProperty(this,"destroy",{get:function(){return P}}),Object.defineProperty(this,"reveal",{get:function(){return L}}),Object.defineProperty(this,"clean",{get:function(){return M}}),Object.defineProperty(this,"sync",{get:function(){return I}}),Object.defineProperty(this,"defaults",{get:function(){return C}}),Object.defineProperty(this,"version",{get:function(){return $}}),Object.defineProperty(this,"noop",{get:function(){return!1}}),Y||(Y=this))}return H.isSupported=function(){return("transform"in(t=document.documentElement.style)||"WebkitTransform"in t)&&("transition"in(e=document.documentElement.style)||"WebkitTransition"in e);var e,t},Object.defineProperty(H,"debug",{get:function(){return W||!1},set:function(e){return W="boolean"==typeof e?e:W}}),H(),H}(); 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrollreveal", 3 | "version": "4.0.9", 4 | "description": "Animate elements as they scroll into view", 5 | "homepage": "https://scrollrevealjs.org", 6 | "main": "dist/scrollreveal.js", 7 | "module": "dist/scrollreveal.es.js", 8 | "jsnext:main": "dist/scrollreveal.es.js", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "prebuild": "rm -rf dist/*", 14 | "build": "npm run bundle && npm run bundle:min", 15 | "bundle": "./node_modules/rollup/bin/rollup -c ./build/rollup.conf.js", 16 | "bundle:min": "./node_modules/rollup/bin/rollup -c ./build/rollup.conf.min.js", 17 | "lint": "./node_modules/eslint/bin/eslint.js src test", 18 | "pretest": "rm -rf .ignore/coverage/**/ && npm run lint", 19 | "test": "./node_modules/karma/bin/karma start ./test/karma.conf.js", 20 | "testing": "cross-env COVERAGE=true npm test -- --no-single-run", 21 | "coverage": "cross-env COVERAGE=true npm test", 22 | "sandbox:bundle": "./node_modules/rollup/bin/rollup -w -c ./.ignore/sandbox/rollup.conf.sandbox.js", 23 | "sandbox:server": "node ./.ignore/sandbox/server.sandbox.js", 24 | "coverage:server": "node ./.ignore/coverage/server.coverage.js", 25 | "postinstall": "node -e \"console.log('\\u001b[35m\\u001b[1mLove ScrollReveal? 🔑 Buy a license!\\u001b[22m\\u001b[35m\\n >> \\u001b[33mhttps://scrollrevealjs.org/pricing/\\u001b[0m\\n')\"" 26 | }, 27 | "keywords": [ 28 | "scroll", 29 | "animation", 30 | "reveal", 31 | "css", 32 | "transform", 33 | "transition" 34 | ], 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/jlmakes/scrollreveal.git" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/jlmakes/scrollreveal/issues" 41 | }, 42 | "dependencies": { 43 | "miniraf": "1.0.0", 44 | "rematrix": "0.3.0", 45 | "tealight": "0.3.6" 46 | }, 47 | "devDependencies": { 48 | "chai": "^4.1.2", 49 | "cross-env": "^5.1.3", 50 | "eslint": "^4.16.0", 51 | "karma": "^2.0.0", 52 | "karma-chrome-launcher": "^2.0.0", 53 | "karma-coverage": "^1.1.1", 54 | "karma-coveralls": "^1.1.2", 55 | "karma-mocha": "^1.3.0", 56 | "karma-mocha-reporter": "^2.2.5", 57 | "karma-rollup-preprocessor": "^5.1.1", 58 | "karma-sauce-launcher": "^1.1.0", 59 | "karma-sinon-chai": "^1.3.3", 60 | "live-server": "jlmakes/live-server", 61 | "mocha": "^4.0.1", 62 | "rollup": "^0.55.0", 63 | "rollup-plugin-buble": "^0.x", 64 | "rollup-plugin-istanbul": "^1.1.0", 65 | "rollup-plugin-json": "^2.1.0", 66 | "rollup-plugin-node-resolve": "^3.0.0", 67 | "rollup-plugin-strip": "^1.1.1", 68 | "rollup-plugin-uglify": "^2.0.1", 69 | "rollup-watch": "^4.3.1", 70 | "sinon": "^4.2.0", 71 | "sinon-chai": "^2.8.0" 72 | }, 73 | "author": "Julian Lloyd", 74 | "license": "GPL-3.0" 75 | } 76 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Constructor from './instance/constructor' 2 | 3 | Constructor() 4 | 5 | export default Constructor 6 | -------------------------------------------------------------------------------- /src/instance/constructor.js: -------------------------------------------------------------------------------- 1 | import defaults from './defaults' 2 | import mount from './mount' 3 | 4 | import clean from './methods/clean' 5 | import destroy from './methods/destroy' 6 | import reveal from './methods/reveal' 7 | import sync from './methods/sync' 8 | 9 | import delegate from './functions/delegate' 10 | 11 | import isMobile from '../utils/is-mobile' 12 | import isTransformSupported from '../utils/is-transform-supported' 13 | import isTransitionSupported from '../utils/is-transition-supported' 14 | 15 | import deepAssign from '../utils/deep-assign' 16 | import logger from '../utils/logger' 17 | import $ from 'tealight' 18 | 19 | import { version } from '../../package.json' 20 | 21 | let boundDelegate 22 | let boundDestroy 23 | let boundReveal 24 | let boundClean 25 | let boundSync 26 | let config 27 | let debug 28 | let instance 29 | 30 | export default function ScrollReveal(options = {}) { 31 | const invokedWithoutNew = 32 | typeof this === 'undefined' || 33 | Object.getPrototypeOf(this) !== ScrollReveal.prototype 34 | 35 | if (invokedWithoutNew) { 36 | return new ScrollReveal(options) 37 | } 38 | 39 | if (!ScrollReveal.isSupported()) { 40 | logger.call(this, 'Instantiation failed.', 'This browser is not supported.') 41 | return mount.failure() 42 | } 43 | 44 | let buffer 45 | try { 46 | buffer = config 47 | ? deepAssign({}, config, options) 48 | : deepAssign({}, defaults, options) 49 | } catch (e) { 50 | logger.call(this, 'Invalid configuration.', e.message) 51 | return mount.failure() 52 | } 53 | 54 | try { 55 | const container = $(buffer.container)[0] 56 | if (!container) { 57 | throw new Error('Invalid container.') 58 | } 59 | } catch (e) { 60 | logger.call(this, e.message) 61 | return mount.failure() 62 | } 63 | 64 | config = buffer 65 | 66 | if ((!config.mobile && isMobile()) || (!config.desktop && !isMobile())) { 67 | logger.call( 68 | this, 69 | 'This device is disabled.', 70 | `desktop: ${config.desktop}`, 71 | `mobile: ${config.mobile}` 72 | ) 73 | return mount.failure() 74 | } 75 | 76 | mount.success() 77 | 78 | this.store = { 79 | containers: {}, 80 | elements: {}, 81 | history: [], 82 | sequences: {} 83 | } 84 | 85 | this.pristine = true 86 | 87 | boundDelegate = boundDelegate || delegate.bind(this) 88 | boundDestroy = boundDestroy || destroy.bind(this) 89 | boundReveal = boundReveal || reveal.bind(this) 90 | boundClean = boundClean || clean.bind(this) 91 | boundSync = boundSync || sync.bind(this) 92 | 93 | Object.defineProperty(this, 'delegate', { get: () => boundDelegate }) 94 | Object.defineProperty(this, 'destroy', { get: () => boundDestroy }) 95 | Object.defineProperty(this, 'reveal', { get: () => boundReveal }) 96 | Object.defineProperty(this, 'clean', { get: () => boundClean }) 97 | Object.defineProperty(this, 'sync', { get: () => boundSync }) 98 | 99 | Object.defineProperty(this, 'defaults', { get: () => config }) 100 | Object.defineProperty(this, 'version', { get: () => version }) 101 | Object.defineProperty(this, 'noop', { get: () => false }) 102 | 103 | return instance ? instance : (instance = this) 104 | } 105 | 106 | ScrollReveal.isSupported = () => 107 | isTransformSupported() && isTransitionSupported() 108 | 109 | Object.defineProperty(ScrollReveal, 'debug', { 110 | get: () => debug || false, 111 | set: value => (debug = typeof value === 'boolean' ? value : debug) 112 | }) 113 | -------------------------------------------------------------------------------- /src/instance/defaults.js: -------------------------------------------------------------------------------- 1 | export default { 2 | delay: 0, 3 | distance: '0', 4 | duration: 600, 5 | easing: 'cubic-bezier(0.5, 0, 0, 1)', 6 | interval: 0, 7 | opacity: 0, 8 | origin: 'bottom', 9 | rotate: { 10 | x: 0, 11 | y: 0, 12 | z: 0 13 | }, 14 | scale: 1, 15 | cleanup: false, 16 | container: document.documentElement, 17 | desktop: true, 18 | mobile: true, 19 | reset: false, 20 | useDelay: 'always', 21 | viewFactor: 0.0, 22 | viewOffset: { 23 | top: 0, 24 | right: 0, 25 | bottom: 0, 26 | left: 0 27 | }, 28 | afterReset() {}, 29 | afterReveal() {}, 30 | beforeReset() {}, 31 | beforeReveal() {} 32 | } 33 | -------------------------------------------------------------------------------- /src/instance/functions/animate.js: -------------------------------------------------------------------------------- 1 | import { applyStyle } from '../functions/style' 2 | import clean from '../methods/clean' 3 | 4 | export default function animate(element, force = {}) { 5 | const pristine = force.pristine || this.pristine 6 | const delayed = 7 | element.config.useDelay === 'always' || 8 | (element.config.useDelay === 'onload' && pristine) || 9 | (element.config.useDelay === 'once' && !element.seen) 10 | 11 | const shouldReveal = element.visible && !element.revealed 12 | const shouldReset = !element.visible && element.revealed && element.config.reset 13 | 14 | if (force.reveal || shouldReveal) { 15 | return triggerReveal.call(this, element, delayed) 16 | } 17 | 18 | if (force.reset || shouldReset) { 19 | return triggerReset.call(this, element) 20 | } 21 | } 22 | 23 | function triggerReveal(element, delayed) { 24 | const styles = [ 25 | element.styles.inline.generated, 26 | element.styles.opacity.computed, 27 | element.styles.transform.generated.final 28 | ] 29 | if (delayed) { 30 | styles.push(element.styles.transition.generated.delayed) 31 | } else { 32 | styles.push(element.styles.transition.generated.instant) 33 | } 34 | element.revealed = element.seen = true 35 | applyStyle(element.node, styles.filter((s) => s !== '').join(' ')) 36 | registerCallbacks.call(this, element, delayed) 37 | } 38 | 39 | function triggerReset(element) { 40 | const styles = [ 41 | element.styles.inline.generated, 42 | element.styles.opacity.generated, 43 | element.styles.transform.generated.initial, 44 | element.styles.transition.generated.instant 45 | ] 46 | element.revealed = false 47 | applyStyle(element.node, styles.filter((s) => s !== '').join(' ')) 48 | registerCallbacks.call(this, element) 49 | } 50 | 51 | function registerCallbacks(element, isDelayed) { 52 | const duration = isDelayed 53 | ? element.config.duration + element.config.delay 54 | : element.config.duration 55 | 56 | const beforeCallback = element.revealed 57 | ? element.config.beforeReveal 58 | : element.config.beforeReset 59 | 60 | const afterCallback = element.revealed 61 | ? element.config.afterReveal 62 | : element.config.afterReset 63 | 64 | let elapsed = 0 65 | if (element.callbackTimer) { 66 | elapsed = Date.now() - element.callbackTimer.start 67 | window.clearTimeout(element.callbackTimer.clock) 68 | } 69 | 70 | beforeCallback(element.node) 71 | 72 | element.callbackTimer = { 73 | start: Date.now(), 74 | clock: window.setTimeout(() => { 75 | afterCallback(element.node) 76 | element.callbackTimer = null 77 | if (element.revealed && !element.config.reset && element.config.cleanup) { 78 | clean.call(this, element.node) 79 | } 80 | }, duration - elapsed) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/instance/functions/delegate.js: -------------------------------------------------------------------------------- 1 | import animate from './animate' 2 | import sequence from './sequence' 3 | import mathSign from '../../polyfills/math-sign' 4 | import raf from 'miniraf' 5 | import each from '../../utils/each' 6 | import getGeometry from '../../utils/get-geometry' 7 | import getScrolled from '../../utils/get-scrolled' 8 | import isElementVisible from '../../utils/is-element-visible' 9 | 10 | export default function delegate( 11 | event = { type: 'init' }, 12 | elements = this.store.elements 13 | ) { 14 | raf(() => { 15 | const stale = event.type === 'init' || event.type === 'resize' 16 | 17 | each(this.store.containers, container => { 18 | if (stale) { 19 | container.geometry = getGeometry.call(this, container, true) 20 | } 21 | const scroll = getScrolled.call(this, container) 22 | if (container.scroll) { 23 | container.direction = { 24 | x: mathSign(scroll.left - container.scroll.left), 25 | y: mathSign(scroll.top - container.scroll.top) 26 | } 27 | } 28 | container.scroll = scroll 29 | }) 30 | 31 | /** 32 | * Due to how the sequencer is implemented, it’s 33 | * important that we update the state of all 34 | * elements, before any animation logic is 35 | * evaluated (in the second loop below). 36 | */ 37 | each(elements, element => { 38 | if (stale || element.geometry === undefined) { 39 | element.geometry = getGeometry.call(this, element) 40 | } 41 | element.visible = isElementVisible.call(this, element) 42 | }) 43 | 44 | each(elements, element => { 45 | if (element.sequence) { 46 | sequence.call(this, element) 47 | } else { 48 | animate.call(this, element) 49 | } 50 | }) 51 | 52 | this.pristine = false 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /src/instance/functions/initialize.js: -------------------------------------------------------------------------------- 1 | import each from '../../utils/each' 2 | import { applyStyle } from '../functions/style' 3 | import rinse from './rinse' 4 | 5 | export default function initialize() { 6 | rinse.call(this) 7 | 8 | each(this.store.elements, element => { 9 | let styles = [element.styles.inline.generated] 10 | 11 | if (element.visible) { 12 | styles.push(element.styles.opacity.computed) 13 | styles.push(element.styles.transform.generated.final) 14 | element.revealed = true 15 | } else { 16 | styles.push(element.styles.opacity.generated) 17 | styles.push(element.styles.transform.generated.initial) 18 | element.revealed = false 19 | } 20 | 21 | applyStyle(element.node, styles.filter((s) => s !== '').join(' ')) 22 | }) 23 | 24 | each(this.store.containers, container => { 25 | const target = 26 | container.node === document.documentElement ? window : container.node 27 | target.addEventListener('scroll', this.delegate) 28 | target.addEventListener('resize', this.delegate) 29 | }) 30 | 31 | /** 32 | * Manually invoke delegate once to capture 33 | * element and container dimensions, container 34 | * scroll position, and trigger any valid reveals 35 | */ 36 | this.delegate() 37 | 38 | /** 39 | * Wipe any existing `setTimeout` now 40 | * that initialization has completed. 41 | */ 42 | this.initTimeout = null 43 | } 44 | -------------------------------------------------------------------------------- /src/instance/functions/rinse.js: -------------------------------------------------------------------------------- 1 | import $ from 'tealight' 2 | import each from '../../utils/each' 3 | 4 | export default function rinse() { 5 | const struct = () => ({ 6 | active: [], 7 | stale: [] 8 | }) 9 | 10 | const elementIds = struct() 11 | const sequenceIds = struct() 12 | const containerIds = struct() 13 | 14 | /** 15 | * Take stock of active element IDs. 16 | */ 17 | try { 18 | each($('[data-sr-id]'), node => { 19 | const id = parseInt(node.getAttribute('data-sr-id')) 20 | elementIds.active.push(id) 21 | }) 22 | } catch (e) { 23 | throw e 24 | } 25 | /** 26 | * Destroy stale elements. 27 | */ 28 | each(this.store.elements, element => { 29 | if (elementIds.active.indexOf(element.id) === -1) { 30 | elementIds.stale.push(element.id) 31 | } 32 | }) 33 | 34 | each(elementIds.stale, staleId => delete this.store.elements[staleId]) 35 | 36 | /** 37 | * Take stock of active container and sequence IDs. 38 | */ 39 | each(this.store.elements, element => { 40 | if (containerIds.active.indexOf(element.containerId) === -1) { 41 | containerIds.active.push(element.containerId) 42 | } 43 | if (element.hasOwnProperty('sequence')) { 44 | if (sequenceIds.active.indexOf(element.sequence.id) === -1) { 45 | sequenceIds.active.push(element.sequence.id) 46 | } 47 | } 48 | }) 49 | 50 | /** 51 | * Destroy stale containers. 52 | */ 53 | each(this.store.containers, container => { 54 | if (containerIds.active.indexOf(container.id) === -1) { 55 | containerIds.stale.push(container.id) 56 | } 57 | }) 58 | 59 | each(containerIds.stale, staleId => { 60 | const stale = this.store.containers[staleId].node 61 | stale.removeEventListener('scroll', this.delegate) 62 | stale.removeEventListener('resize', this.delegate) 63 | delete this.store.containers[staleId] 64 | }) 65 | 66 | /** 67 | * Destroy stale sequences. 68 | */ 69 | each(this.store.sequences, sequence => { 70 | if (sequenceIds.active.indexOf(sequence.id) === -1) { 71 | sequenceIds.stale.push(sequence.id) 72 | } 73 | }) 74 | 75 | each(sequenceIds.stale, staleId => delete this.store.sequences[staleId]) 76 | } 77 | -------------------------------------------------------------------------------- /src/instance/functions/sequence.js: -------------------------------------------------------------------------------- 1 | import animate from './animate' 2 | import each from '../../utils/each' 3 | import nextUniqueId from '../../utils/next-unique-id' 4 | 5 | export default function sequence(element, pristine = this.pristine) { 6 | /** 7 | * We first check if the element should reset. 8 | */ 9 | if (!element.visible && element.revealed && element.config.reset) { 10 | return animate.call(this, element, { reset: true }) 11 | } 12 | 13 | const seq = this.store.sequences[element.sequence.id] 14 | const i = element.sequence.index 15 | 16 | if (seq) { 17 | const visible = new SequenceModel(seq, 'visible', this.store) 18 | const revealed = new SequenceModel(seq, 'revealed', this.store) 19 | 20 | seq.models = { visible, revealed } 21 | 22 | /** 23 | * If the sequence has no revealed members, 24 | * then we reveal the first visible element 25 | * within that sequence. 26 | * 27 | * The sequence then cues a recursive call 28 | * in both directions. 29 | */ 30 | if (!revealed.body.length) { 31 | const nextId = seq.members[visible.body[0]] 32 | const nextElement = this.store.elements[nextId] 33 | 34 | if (nextElement) { 35 | cue.call(this, seq, visible.body[0], -1, pristine) 36 | cue.call(this, seq, visible.body[0], +1, pristine) 37 | return animate.call(this, nextElement, { reveal: true, pristine }) 38 | } 39 | } 40 | 41 | /** 42 | * If our element isn’t resetting, we check the 43 | * element sequence index against the head, and 44 | * then the foot of the sequence. 45 | */ 46 | if ( 47 | !seq.blocked.head && 48 | i === [...revealed.head].pop() && 49 | i >= [...visible.body].shift() 50 | ) { 51 | cue.call(this, seq, i, -1, pristine) 52 | return animate.call(this, element, { reveal: true, pristine }) 53 | } 54 | 55 | if ( 56 | !seq.blocked.foot && 57 | i === [...revealed.foot].shift() && 58 | i <= [...visible.body].pop() 59 | ) { 60 | cue.call(this, seq, i, +1, pristine) 61 | return animate.call(this, element, { reveal: true, pristine }) 62 | } 63 | } 64 | } 65 | 66 | export function Sequence(interval) { 67 | const i = Math.abs(interval) 68 | if (!isNaN(i)) { 69 | this.id = nextUniqueId() 70 | this.interval = Math.max(i, 16) 71 | this.members = [] 72 | this.models = {} 73 | this.blocked = { 74 | head: false, 75 | foot: false 76 | } 77 | } else { 78 | throw new RangeError('Invalid sequence interval.') 79 | } 80 | } 81 | 82 | function SequenceModel(seq, prop, store) { 83 | this.head = [] 84 | this.body = [] 85 | this.foot = [] 86 | 87 | each(seq.members, (id, index) => { 88 | const element = store.elements[id] 89 | if (element && element[prop]) { 90 | this.body.push(index) 91 | } 92 | }) 93 | 94 | if (this.body.length) { 95 | each(seq.members, (id, index) => { 96 | const element = store.elements[id] 97 | if (element && !element[prop]) { 98 | if (index < this.body[0]) { 99 | this.head.push(index) 100 | } else { 101 | this.foot.push(index) 102 | } 103 | } 104 | }) 105 | } 106 | } 107 | 108 | function cue(seq, i, direction, pristine) { 109 | const blocked = ['head', null, 'foot'][1 + direction] 110 | const nextId = seq.members[i + direction] 111 | const nextElement = this.store.elements[nextId] 112 | 113 | seq.blocked[blocked] = true 114 | 115 | setTimeout(() => { 116 | seq.blocked[blocked] = false 117 | if (nextElement) { 118 | sequence.call(this, nextElement, pristine) 119 | } 120 | }, seq.interval) 121 | } 122 | -------------------------------------------------------------------------------- /src/instance/functions/style.js: -------------------------------------------------------------------------------- 1 | import { 2 | multiply, 3 | parse, 4 | rotateX, 5 | rotateY, 6 | rotateZ, 7 | scale, 8 | translateX, 9 | translateY 10 | } from 'rematrix' 11 | import getPrefixedCssProp from '../../utils/get-prefixed-css-prop' 12 | 13 | export default function style(element) { 14 | const computed = window.getComputedStyle(element.node) 15 | const position = computed.position 16 | const config = element.config 17 | 18 | /** 19 | * Generate inline styles 20 | */ 21 | const inline = {} 22 | const inlineStyle = element.node.getAttribute('style') || '' 23 | const inlineMatch = inlineStyle.match(/[\w-]+\s*:\s*[^;]+\s*/gi) || [] 24 | 25 | inline.computed = inlineMatch ? inlineMatch.map(m => m.trim()).join('; ') + ';' : '' 26 | 27 | inline.generated = inlineMatch.some(m => m.match(/visibility\s?:\s?visible/i)) 28 | ? inline.computed 29 | : [...inlineMatch, 'visibility: visible'].map(m => m.trim()).join('; ') + ';' 30 | 31 | /** 32 | * Generate opacity styles 33 | */ 34 | const computedOpacity = parseFloat(computed.opacity) 35 | const configOpacity = !isNaN(parseFloat(config.opacity)) 36 | ? parseFloat(config.opacity) 37 | : parseFloat(computed.opacity) 38 | 39 | const opacity = { 40 | computed: computedOpacity !== configOpacity ? `opacity: ${computedOpacity};` : '', 41 | generated: computedOpacity !== configOpacity ? `opacity: ${configOpacity};` : '' 42 | } 43 | 44 | /** 45 | * Generate transformation styles 46 | */ 47 | const transformations = [] 48 | 49 | if (parseFloat(config.distance)) { 50 | const axis = config.origin === 'top' || config.origin === 'bottom' ? 'Y' : 'X' 51 | 52 | /** 53 | * Let’s make sure our our pixel distances are negative for top and left. 54 | * e.g. { origin: 'top', distance: '25px' } starts at `top: -25px` in CSS. 55 | */ 56 | let distance = config.distance 57 | if (config.origin === 'top' || config.origin === 'left') { 58 | distance = /^-/.test(distance) ? distance.substr(1) : `-${distance}` 59 | } 60 | 61 | const [value, unit] = distance.match(/(^-?\d+\.?\d?)|(em$|px$|%$)/g) 62 | 63 | switch (unit) { 64 | case 'em': 65 | distance = parseInt(computed.fontSize) * value 66 | break 67 | case 'px': 68 | distance = value 69 | break 70 | case '%': 71 | /** 72 | * Here we use `getBoundingClientRect` instead of 73 | * the existing data attached to `element.geometry` 74 | * because only the former includes any transformations 75 | * current applied to the element. 76 | * 77 | * If that behavior ends up being unintuitive, this 78 | * logic could instead utilize `element.geometry.height` 79 | * and `element.geoemetry.width` for the distance calculation 80 | */ 81 | distance = 82 | axis === 'Y' 83 | ? (element.node.getBoundingClientRect().height * value) / 100 84 | : (element.node.getBoundingClientRect().width * value) / 100 85 | break 86 | default: 87 | throw new RangeError('Unrecognized or missing distance unit.') 88 | } 89 | 90 | if (axis === 'Y') { 91 | transformations.push(translateY(distance)) 92 | } else { 93 | transformations.push(translateX(distance)) 94 | } 95 | } 96 | 97 | if (config.rotate.x) transformations.push(rotateX(config.rotate.x)) 98 | if (config.rotate.y) transformations.push(rotateY(config.rotate.y)) 99 | if (config.rotate.z) transformations.push(rotateZ(config.rotate.z)) 100 | if (config.scale !== 1) { 101 | if (config.scale === 0) { 102 | /** 103 | * The CSS Transforms matrix interpolation specification 104 | * basically disallows transitions of non-invertible 105 | * matrixes, which means browsers won't transition 106 | * elements with zero scale. 107 | * 108 | * That’s inconvenient for the API and developer 109 | * experience, so we simply nudge their value 110 | * slightly above zero; this allows browsers 111 | * to transition our element as expected. 112 | * 113 | * `0.0002` was the smallest number 114 | * that performed across browsers. 115 | */ 116 | transformations.push(scale(0.0002)) 117 | } else { 118 | transformations.push(scale(config.scale)) 119 | } 120 | } 121 | 122 | const transform = {} 123 | if (transformations.length) { 124 | transform.property = getPrefixedCssProp('transform') 125 | /** 126 | * The default computed transform value should be one of: 127 | * undefined || 'none' || 'matrix()' || 'matrix3d()' 128 | */ 129 | transform.computed = { 130 | raw: computed[transform.property], 131 | matrix: parse(computed[transform.property]) 132 | } 133 | 134 | transformations.unshift(transform.computed.matrix) 135 | const product = transformations.reduce(multiply) 136 | 137 | transform.generated = { 138 | initial: `${transform.property}: matrix3d(${product.join(', ')});`, 139 | final: `${transform.property}: matrix3d(${transform.computed.matrix.join(', ')});` 140 | } 141 | } else { 142 | transform.generated = { 143 | initial: '', 144 | final: '' 145 | } 146 | } 147 | 148 | /** 149 | * Generate transition styles 150 | */ 151 | let transition = {} 152 | if (opacity.generated || transform.generated.initial) { 153 | transition.property = getPrefixedCssProp('transition') 154 | transition.computed = computed[transition.property] 155 | transition.fragments = [] 156 | 157 | const { delay, duration, easing } = config 158 | 159 | if (opacity.generated) { 160 | transition.fragments.push({ 161 | delayed: `opacity ${duration / 1000}s ${easing} ${delay / 1000}s`, 162 | instant: `opacity ${duration / 1000}s ${easing} 0s` 163 | }) 164 | } 165 | 166 | if (transform.generated.initial) { 167 | transition.fragments.push({ 168 | delayed: `${transform.property} ${duration / 1000}s ${easing} ${delay / 1000}s`, 169 | instant: `${transform.property} ${duration / 1000}s ${easing} 0s` 170 | }) 171 | } 172 | 173 | /** 174 | * The default computed transition property should be undefined, or one of: 175 | * '' || 'none 0s ease 0s' || 'all 0s ease 0s' || 'all 0s 0s cubic-bezier()' 176 | */ 177 | let hasCustomTransition = 178 | transition.computed && !transition.computed.match(/all 0s|none 0s/) 179 | 180 | if (hasCustomTransition) { 181 | transition.fragments.unshift({ 182 | delayed: transition.computed, 183 | instant: transition.computed 184 | }) 185 | } 186 | 187 | const composed = transition.fragments.reduce( 188 | (composition, fragment, i) => { 189 | composition.delayed += i === 0 ? fragment.delayed : `, ${fragment.delayed}` 190 | composition.instant += i === 0 ? fragment.instant : `, ${fragment.instant}` 191 | return composition 192 | }, 193 | { 194 | delayed: '', 195 | instant: '' 196 | } 197 | ) 198 | 199 | transition.generated = { 200 | delayed: `${transition.property}: ${composed.delayed};`, 201 | instant: `${transition.property}: ${composed.instant};` 202 | } 203 | } else { 204 | transition.generated = { 205 | delayed: '', 206 | instant: '' 207 | } 208 | } 209 | 210 | return { 211 | inline, 212 | opacity, 213 | position, 214 | transform, 215 | transition 216 | } 217 | } 218 | 219 | /** 220 | * apply a CSS string to an element using the CSSOM (element.style) rather 221 | * than setAttribute, which may violate the content security policy. 222 | * 223 | * @param {Node} [el] Element to receive styles. 224 | * @param {string} [declaration] Styles to apply. 225 | */ 226 | export function applyStyle (el, declaration) { 227 | declaration.split(';').forEach(pair => { 228 | const [property, ...value] = pair.split(':') 229 | if (property && value) { 230 | el.style[property.trim()] = value.join(':') 231 | } 232 | }) 233 | } 234 | 235 | -------------------------------------------------------------------------------- /src/instance/methods/clean.js: -------------------------------------------------------------------------------- 1 | import $ from 'tealight' 2 | import each from '../../utils/each' 3 | import logger from '../../utils/logger' 4 | import rinse from '../functions/rinse' 5 | import { applyStyle } from '../functions/style' 6 | 7 | export default function clean(target) { 8 | let dirty 9 | try { 10 | each($(target), node => { 11 | const id = node.getAttribute('data-sr-id') 12 | if (id !== null) { 13 | dirty = true 14 | const element = this.store.elements[id] 15 | if (element.callbackTimer) { 16 | window.clearTimeout(element.callbackTimer.clock) 17 | } 18 | applyStyle(element.node, element.styles.inline.generated) 19 | node.removeAttribute('data-sr-id') 20 | delete this.store.elements[id] 21 | } 22 | }) 23 | } catch (e) { 24 | return logger.call(this, 'Clean failed.', e.message) 25 | } 26 | 27 | if (dirty) { 28 | try { 29 | rinse.call(this) 30 | } catch (e) { 31 | return logger.call(this, 'Clean failed.', e.message) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/instance/methods/destroy.js: -------------------------------------------------------------------------------- 1 | import each from '../../utils/each' 2 | import { applyStyle } from '../functions/style' 3 | 4 | export default function destroy() { 5 | /** 6 | * Remove all generated styles and element ids 7 | */ 8 | each(this.store.elements, element => { 9 | applyStyle(element.node, element.styles.inline.generated) 10 | element.node.removeAttribute('data-sr-id') 11 | }) 12 | 13 | /** 14 | * Remove all event listeners. 15 | */ 16 | each(this.store.containers, container => { 17 | const target = 18 | container.node === document.documentElement ? window : container.node 19 | target.removeEventListener('scroll', this.delegate) 20 | target.removeEventListener('resize', this.delegate) 21 | }) 22 | 23 | /** 24 | * Clear all data from the store 25 | */ 26 | this.store = { 27 | containers: {}, 28 | elements: {}, 29 | history: [], 30 | sequences: {} 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/instance/methods/reveal.js: -------------------------------------------------------------------------------- 1 | import tealight from 'tealight' 2 | import deepAssign from '../../utils/deep-assign' 3 | import each from '../../utils/each' 4 | import isMobile from '../../utils/is-mobile' 5 | import logger from '../../utils/logger' 6 | import nextUniqueId from '../../utils/next-unique-id' 7 | import defaults from '../defaults' 8 | import initialize from '../functions/initialize' 9 | import { Sequence } from '../functions/sequence' 10 | import style, { applyStyle } from '../functions/style' 11 | import clean from '../methods/clean' 12 | 13 | export default function reveal(target, options = {}, syncing = false) { 14 | const containerBuffer = [] 15 | let sequence 16 | let interval = options.interval || defaults.interval 17 | 18 | try { 19 | if (interval) { 20 | sequence = new Sequence(interval) 21 | } 22 | 23 | const nodes = tealight(target) 24 | if (!nodes.length) { 25 | throw new Error('Invalid reveal target.') 26 | } 27 | 28 | const elements = nodes.reduce((elementBuffer, elementNode) => { 29 | const element = {} 30 | const existingId = elementNode.getAttribute('data-sr-id') 31 | 32 | if (existingId) { 33 | deepAssign(element, this.store.elements[existingId]) 34 | 35 | /** 36 | * In order to prevent previously generated styles 37 | * from throwing off the new styles, the style tag 38 | * has to be reverted to its pre-reveal state. 39 | */ 40 | applyStyle(element.node, element.styles.inline.computed) 41 | } else { 42 | element.id = nextUniqueId() 43 | element.node = elementNode 44 | element.seen = false 45 | element.revealed = false 46 | element.visible = false 47 | } 48 | 49 | const config = deepAssign({}, element.config || this.defaults, options) 50 | 51 | if ((!config.mobile && isMobile()) || (!config.desktop && !isMobile())) { 52 | if (existingId) { 53 | clean.call(this, element) 54 | } 55 | return elementBuffer // skip elements that are disabled 56 | } 57 | 58 | const containerNode = tealight(config.container)[0] 59 | if (!containerNode) { 60 | throw new Error('Invalid container.') 61 | } 62 | if (!containerNode.contains(elementNode)) { 63 | return elementBuffer // skip elements found outside the container 64 | } 65 | 66 | let containerId 67 | { 68 | containerId = getContainerId( 69 | containerNode, 70 | containerBuffer, 71 | this.store.containers 72 | ) 73 | if (containerId === null) { 74 | containerId = nextUniqueId() 75 | containerBuffer.push({ id: containerId, node: containerNode }) 76 | } 77 | } 78 | 79 | element.config = config 80 | element.containerId = containerId 81 | element.styles = style(element) 82 | 83 | if (sequence) { 84 | element.sequence = { 85 | id: sequence.id, 86 | index: sequence.members.length 87 | } 88 | sequence.members.push(element.id) 89 | } 90 | 91 | elementBuffer.push(element) 92 | return elementBuffer 93 | }, []) 94 | 95 | /** 96 | * Modifying the DOM via setAttribute needs to be handled 97 | * separately from reading computed styles in the map above 98 | * for the browser to batch DOM changes (limiting reflows) 99 | */ 100 | each(elements, element => { 101 | this.store.elements[element.id] = element 102 | element.node.setAttribute('data-sr-id', element.id) 103 | }) 104 | } catch (e) { 105 | return logger.call(this, 'Reveal failed.', e.message) 106 | } 107 | 108 | /** 109 | * Now that element set-up is complete... 110 | * Let’s commit any container and sequence data we have to the store. 111 | */ 112 | each(containerBuffer, container => { 113 | this.store.containers[container.id] = { 114 | id: container.id, 115 | node: container.node 116 | } 117 | }) 118 | if (sequence) { 119 | this.store.sequences[sequence.id] = sequence 120 | } 121 | 122 | /** 123 | * If reveal wasn't invoked by sync, we want to 124 | * make sure to add this call to the history. 125 | */ 126 | if (syncing !== true) { 127 | this.store.history.push({ target, options }) 128 | 129 | /** 130 | * Push initialization to the event queue, giving 131 | * multiple reveal calls time to be interpreted. 132 | */ 133 | if (this.initTimeout) { 134 | window.clearTimeout(this.initTimeout) 135 | } 136 | this.initTimeout = window.setTimeout(initialize.bind(this), 0) 137 | } 138 | } 139 | 140 | function getContainerId(node, ...collections) { 141 | let id = null 142 | each(collections, collection => { 143 | each(collection, container => { 144 | if (id === null && container.node === node) { 145 | id = container.id 146 | } 147 | }) 148 | }) 149 | return id 150 | } 151 | -------------------------------------------------------------------------------- /src/instance/methods/sync.js: -------------------------------------------------------------------------------- 1 | import initialize from '../functions/initialize' 2 | import each from '../../utils/each' 3 | import reveal from './reveal' 4 | 5 | /** 6 | * Re-runs the reveal method for each record stored in history, 7 | * for capturing new content asynchronously loaded into the DOM. 8 | */ 9 | export default function sync() { 10 | each(this.store.history, record => { 11 | reveal.call(this, record.target, record.options, true) 12 | }) 13 | 14 | initialize.call(this) 15 | } 16 | -------------------------------------------------------------------------------- /src/instance/mount.js: -------------------------------------------------------------------------------- 1 | function failure() { 2 | document.documentElement.classList.remove('sr') 3 | 4 | return { 5 | clean() {}, 6 | destroy() {}, 7 | reveal() {}, 8 | sync() {}, 9 | get noop() { 10 | return true 11 | } 12 | } 13 | } 14 | 15 | function success() { 16 | document.documentElement.classList.add('sr') 17 | 18 | if (document.body) { 19 | document.body.style.height = '100%' 20 | } else { 21 | document.addEventListener('DOMContentLoaded', () => { 22 | document.body.style.height = '100%' 23 | }) 24 | } 25 | } 26 | 27 | export default { success, failure } 28 | -------------------------------------------------------------------------------- /src/polyfills/math-sign.js: -------------------------------------------------------------------------------- 1 | export const polyfill = x => (x > 0) - (x < 0) || +x 2 | export default Math.sign || polyfill 3 | -------------------------------------------------------------------------------- /src/utils/deep-assign.js: -------------------------------------------------------------------------------- 1 | import isObject from './is-object' 2 | import each from './each' 3 | 4 | export default function deepAssign(target, ...sources) { 5 | if (isObject(target)) { 6 | each(sources, source => { 7 | each(source, (data, key) => { 8 | if (isObject(data)) { 9 | if (!target[key] || !isObject(target[key])) { 10 | target[key] = {} 11 | } 12 | deepAssign(target[key], data) 13 | } else { 14 | target[key] = data 15 | } 16 | }) 17 | }) 18 | return target 19 | } else { 20 | throw new TypeError('Target must be an object literal.') 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/each.js: -------------------------------------------------------------------------------- 1 | import isObject from './is-object' 2 | 3 | export default function each(collection, callback) { 4 | if (isObject(collection)) { 5 | const keys = Object.keys(collection) 6 | return keys.forEach(key => callback(collection[key], key, collection)) 7 | } 8 | if (collection instanceof Array) { 9 | return collection.forEach((item, i) => callback(item, i, collection)) 10 | } 11 | throw new TypeError('Expected either an array or object literal.') 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/get-geometry.js: -------------------------------------------------------------------------------- 1 | export default function getGeometry(target, isContainer) { 2 | /** 3 | * We want to ignore padding and scrollbars for container elements. 4 | * More information here: https://goo.gl/vOZpbz 5 | */ 6 | const height = isContainer ? target.node.clientHeight : target.node.offsetHeight 7 | const width = isContainer ? target.node.clientWidth : target.node.offsetWidth 8 | 9 | let offsetTop = 0 10 | let offsetLeft = 0 11 | let node = target.node 12 | 13 | do { 14 | if (!isNaN(node.offsetTop)) { 15 | offsetTop += node.offsetTop 16 | } 17 | if (!isNaN(node.offsetLeft)) { 18 | offsetLeft += node.offsetLeft 19 | } 20 | node = node.offsetParent 21 | } while (node) 22 | 23 | return { 24 | bounds: { 25 | top: offsetTop, 26 | right: offsetLeft + width, 27 | bottom: offsetTop + height, 28 | left: offsetLeft 29 | }, 30 | height, 31 | width 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/get-prefixed-css-prop.js: -------------------------------------------------------------------------------- 1 | const getPrefixedCssProp = (() => { 2 | let properties = {} 3 | const style = document.documentElement.style 4 | 5 | function getPrefixedCssProperty(name, source = style) { 6 | if (name && typeof name === 'string') { 7 | if (properties[name]) { 8 | return properties[name] 9 | } 10 | if (typeof source[name] === 'string') { 11 | return (properties[name] = name) 12 | } 13 | if (typeof source[`-webkit-${name}`] === 'string') { 14 | return (properties[name] = `-webkit-${name}`) 15 | } 16 | throw new RangeError(`Unable to find "${name}" style property.`) 17 | } 18 | throw new TypeError('Expected a string.') 19 | } 20 | 21 | getPrefixedCssProperty.clearCache = () => (properties = {}) 22 | 23 | return getPrefixedCssProperty 24 | })() 25 | 26 | export default getPrefixedCssProp 27 | -------------------------------------------------------------------------------- /src/utils/get-scrolled.js: -------------------------------------------------------------------------------- 1 | export default function getScrolled(container) { 2 | let top, left 3 | if (container.node === document.documentElement) { 4 | top = window.pageYOffset 5 | left = window.pageXOffset 6 | } else { 7 | top = container.node.scrollTop 8 | left = container.node.scrollLeft 9 | } 10 | return { top, left } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/is-element-visible.js: -------------------------------------------------------------------------------- 1 | export default function isElementVisible(element = {}) { 2 | const container = this.store.containers[element.containerId] 3 | if (!container) return 4 | 5 | const viewFactor = Math.max(0, Math.min(1, element.config.viewFactor)) 6 | const viewOffset = element.config.viewOffset 7 | 8 | const elementBounds = { 9 | top: element.geometry.bounds.top + element.geometry.height * viewFactor, 10 | right: element.geometry.bounds.right - element.geometry.width * viewFactor, 11 | bottom: element.geometry.bounds.bottom - element.geometry.height * viewFactor, 12 | left: element.geometry.bounds.left + element.geometry.width * viewFactor 13 | } 14 | 15 | const containerBounds = { 16 | top: container.geometry.bounds.top + container.scroll.top + viewOffset.top, 17 | right: container.geometry.bounds.right + container.scroll.left - viewOffset.right, 18 | bottom: 19 | container.geometry.bounds.bottom + container.scroll.top - viewOffset.bottom, 20 | left: container.geometry.bounds.left + container.scroll.left + viewOffset.left 21 | } 22 | 23 | return ( 24 | (elementBounds.top < containerBounds.bottom && 25 | elementBounds.right > containerBounds.left && 26 | elementBounds.bottom > containerBounds.top && 27 | elementBounds.left < containerBounds.right) || 28 | element.styles.position === 'fixed' 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/is-mobile.js: -------------------------------------------------------------------------------- 1 | export default function isMobile(agent = navigator.userAgent) { 2 | return /Android|iPhone|iPad|iPod/i.test(agent) 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/is-object.js: -------------------------------------------------------------------------------- 1 | export default function isObject(x) { 2 | return ( 3 | x !== null && 4 | x instanceof Object && 5 | (x.constructor === Object || 6 | Object.prototype.toString.call(x) === '[object Object]') 7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/is-transform-supported.js: -------------------------------------------------------------------------------- 1 | export default function isTransformSupported() { 2 | const style = document.documentElement.style 3 | return 'transform' in style || 'WebkitTransform' in style 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/is-transition-supported.js: -------------------------------------------------------------------------------- 1 | export default function isTransitionSupported() { 2 | const style = document.documentElement.style 3 | return 'transition' in style || 'WebkitTransition' in style 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | export default function logger(message, ...details) { 2 | if (this.constructor.debug && console) { 3 | let report = `%cScrollReveal: ${message}` 4 | details.forEach(detail => (report += `\n — ${detail}`)) 5 | console.log(report, 'color: #ea654b;') // eslint-disable-line no-console 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/next-unique-id.js: -------------------------------------------------------------------------------- 1 | const nextUniqueId = (() => { 2 | let uid = 0 3 | return () => uid++ 4 | })() 5 | 6 | export default nextUniqueId 7 | -------------------------------------------------------------------------------- /test/instance/constructor.spec.js: -------------------------------------------------------------------------------- 1 | import ScrollReveal from '../../src/instance/constructor' 2 | import isMobile from '../../src/utils/is-mobile' 3 | import { version } from '../../package.json' 4 | 5 | describe('ScrollReveal', () => { 6 | describe('Constructor', () => { 7 | it('should return a new instance with `new` keyword', () => { 8 | const sr = new ScrollReveal() 9 | expect(sr).to.exist 10 | }) 11 | 12 | it('should return a new instance without `new` keyword', () => { 13 | const sr = ScrollReveal() 14 | expect(sr).to.exist 15 | }) 16 | 17 | it('should add the class `sr` to `<html>` element', () => { 18 | document.documentElement.classList.remove('sr') 19 | ScrollReveal() 20 | const result = document.documentElement.classList.contains('sr') 21 | expect(result).to.be.true 22 | }) 23 | 24 | it('should add `height: 100%` to `<body>` element', () => { 25 | document.body.style.height = 'auto' 26 | ScrollReveal() 27 | const result = document.body.style.height === '100%' 28 | expect(result).to.be.true 29 | }) 30 | 31 | it('should return a noop instance when not supported', () => { 32 | const stubs = [ 33 | sinon.stub(console, 'log'), 34 | sinon.stub(ScrollReveal, 'isSupported') 35 | ] 36 | const sr = ScrollReveal() 37 | stubs.forEach(stub => stub.restore()) 38 | expect(sr.noop).to.be.true 39 | }) 40 | 41 | it('should return a noop instance when device is disabled', () => { 42 | isMobile() 43 | ? expect(ScrollReveal({ mobile: false }).noop).to.be.true 44 | : expect(ScrollReveal({ desktop: false }).noop).to.be.true 45 | 46 | ScrollReveal({ desktop: true, mobile: true }) 47 | }) 48 | 49 | it('should return a noop instance when container is invalid', () => { 50 | const stub = sinon.stub(console, 'log') 51 | const sr = ScrollReveal({ container: null }) 52 | stub.restore() 53 | expect(sr.noop).to.be.true 54 | }) 55 | 56 | it('should return a noop instance when passed non-object options', () => { 57 | const stub = sinon.stub(console, 'log') 58 | let sr 59 | { 60 | sr = ScrollReveal(null) 61 | expect(sr.noop).to.be.true 62 | sr = ScrollReveal('foo') 63 | expect(sr.noop).to.be.true 64 | } 65 | stub.restore() 66 | }) 67 | 68 | it('should return a singleton', () => { 69 | const A = ScrollReveal() 70 | const B = ScrollReveal() 71 | expect(A === B).to.be.true 72 | }) 73 | 74 | it('should not update the defaults when re-invoked with invalid options', () => { 75 | ScrollReveal({ duration: 1000 }) 76 | ScrollReveal(null) 77 | expect(ScrollReveal().defaults.duration).to.equal(1000) 78 | }) 79 | 80 | it('should update the defaults when re-invoked with valid options', () => { 81 | ScrollReveal({ duration: 1000 }) 82 | ScrollReveal({ duration: 5000 }) 83 | expect(ScrollReveal().defaults.duration).to.equal(5000) 84 | }) 85 | 86 | it('should have a static `debug` property', () => { 87 | expect(ScrollReveal.debug).to.exist 88 | expect(ScrollReveal.debug).to.be.a('boolean') 89 | }) 90 | 91 | it('should accept boolean value for static `debug` property', () => { 92 | ScrollReveal.debug = true 93 | expect(ScrollReveal.debug).to.be.true 94 | }) 95 | 96 | it('should ignore non-boolean values assigned to static `debug` property', () => { 97 | ScrollReveal.debug = null 98 | expect(ScrollReveal.debug).to.exist 99 | expect(ScrollReveal.debug).to.be.a('boolean') 100 | }) 101 | }) 102 | 103 | describe('Instance', () => { 104 | const sr = new ScrollReveal() 105 | 106 | it('should have a `clean` method', () => { 107 | expect(sr.clean).to.exist 108 | expect(sr.clean).to.be.a('function') 109 | }) 110 | 111 | it('should have a `destroy` method', () => { 112 | expect(sr.destroy).to.exist 113 | expect(sr.destroy).to.be.a('function') 114 | }) 115 | 116 | it('should have a `reveal` method', () => { 117 | expect(sr.reveal).to.exist 118 | expect(sr.reveal).to.be.a('function') 119 | }) 120 | 121 | it('should have a `sync` method', () => { 122 | expect(sr.sync).to.exist 123 | expect(sr.sync).to.be.a('function') 124 | }) 125 | 126 | it('should have a `delegate` property', () => { 127 | expect(sr.delegate).to.exist 128 | expect(sr.delegate).to.be.a('function') 129 | }) 130 | 131 | it('should have a `version` property', () => { 132 | expect(sr.version).to.exist 133 | expect(sr.version).to.be.equal(version) 134 | }) 135 | 136 | it('should have a `noop` property set to `false`', () => { 137 | expect(sr.noop).to.exist 138 | expect(sr.noop).to.be.false 139 | }) 140 | }) 141 | 142 | describe('Non-operational Instance', () => { 143 | const stubs = [ 144 | sinon.stub(console, 'log'), 145 | sinon.stub(ScrollReveal, 'isSupported') 146 | ] 147 | const sr = ScrollReveal() 148 | stubs.forEach(stub => stub.restore()) 149 | 150 | it('should have a `clean` method', () => { 151 | expect(sr.clean).to.exist 152 | expect(sr.clean).to.be.a('function') 153 | }) 154 | 155 | it('should have a `destroy` method', () => { 156 | expect(sr.destroy).to.exist 157 | expect(sr.destroy).to.be.a('function') 158 | }) 159 | 160 | it('should have a `reveal` method', () => { 161 | expect(sr.reveal).to.exist 162 | expect(sr.reveal).to.be.a('function') 163 | }) 164 | 165 | it('should have a `sync` method', () => { 166 | expect(sr.sync).to.exist 167 | expect(sr.sync).to.be.a('function') 168 | }) 169 | 170 | it('should have a `noop` property set to `true`', () => { 171 | expect(sr.noop).to.exist 172 | expect(sr.noop).to.be.true 173 | }) 174 | }) 175 | }) 176 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | const rollupPlugins = [ 2 | require('rollup-plugin-json')(), 3 | require('rollup-plugin-node-resolve')({ jsnext: true, main: true }), 4 | require('rollup-plugin-buble')() 5 | ] 6 | 7 | if (process.env.COVERAGE) { 8 | rollupPlugins.push( 9 | require('rollup-plugin-istanbul')({ 10 | exclude: [ 11 | '../package.json', 12 | '../src/index.js', 13 | './**/*.spec.js', 14 | '**/node_modules/**' 15 | ], 16 | instrumenterConfig: { 17 | embedSource: true 18 | } 19 | }) 20 | ) 21 | } 22 | 23 | module.exports = function(karma) { 24 | karma.set({ 25 | frameworks: ['mocha', 'sinon-chai'], 26 | 27 | preprocessors: { 28 | './**/*.spec.js': ['rollup'] 29 | }, 30 | 31 | files: [{ pattern: './**/*.spec.js', watched: false }], 32 | 33 | rollupPreprocessor: { 34 | plugins: rollupPlugins, 35 | output: { 36 | format: 'iife', 37 | name: 'ScrollReveal', 38 | sourcemap: 'inline' 39 | } 40 | }, 41 | 42 | colors: true, 43 | concurrency: 10, 44 | logLevel: karma.LOG_ERROR, 45 | singleRun: true, 46 | 47 | browserDisconnectTolerance: 1, 48 | browserDisconnectTimeout: 60 * 1000, 49 | browserNoActivityTimeout: 60 * 1000, 50 | // browserNoActivityTimeout: 60 * 1000 * 10 * 6, // dev tools debugging 51 | captureTimeout: 4 * 60 * 1000 52 | }) 53 | 54 | if (process.env.TRAVIS) { 55 | if (process.env.COVERAGE) { 56 | karma.set({ 57 | autoWatch: false, 58 | browsers: ['ChromeHeadless'], 59 | coverageReporter: { 60 | type: 'lcovonly', 61 | dir: 'coverage/' 62 | }, 63 | reporters: ['mocha', 'coverage', 'coveralls'] 64 | }) 65 | } else { 66 | const customLaunchers = require('./sauce.conf') 67 | karma.set({ 68 | autoWatch: false, 69 | browsers: Object.keys(customLaunchers), 70 | customLaunchers, 71 | reporters: ['dots', 'saucelabs'], 72 | hostname: 'localsauce', 73 | sauceLabs: { 74 | testName: 'ScrollReveal', 75 | build: process.env.TRAVIS_BUILD_NUMBER || 'manual', 76 | tunnelIdentifier: process.env.TRAVIS_BUILD_NUMBER || 'autoGeneratedTunnelID', 77 | recordVideo: true, 78 | connectOptions: { 79 | tunnelDomains: 'localsauce' // because Android 8 has an SSL error? 80 | } 81 | } 82 | }) 83 | } 84 | } else { 85 | karma.set({ 86 | browsers: ['ChromeHeadless'], 87 | // browsers: ['Chrome'], // dev tools debugging 88 | coverageReporter: { 89 | type: 'lcov', 90 | dir: '../.ignore/coverage/' 91 | }, 92 | reporters: ['mocha', 'coverage'] 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test/polyfills/math-sign.spec.js: -------------------------------------------------------------------------------- 1 | import { polyfill } from '../../src/polyfills/math-sign' 2 | 3 | describe('Polyfills', () => { 4 | describe('mathSign()', () => { 5 | it('should be a function', () => { 6 | expect(polyfill).to.be.a('function') 7 | }) 8 | 9 | it('should return -1 when passed values smaller than 0', () => { 10 | expect(polyfill(-500)).to.equal(-1) 11 | }) 12 | 13 | it('should return 1 when passed values larger than 0', () => { 14 | expect(polyfill(500)).to.equal(1) 15 | }) 16 | 17 | it('should return 1 when passed true', () => { 18 | expect(polyfill(true)).to.equal(1) 19 | }) 20 | 21 | it('should return -0 when passed -0', () => { 22 | expect(polyfill(-0)).to.equal(-0) 23 | }) 24 | 25 | it('should return 0 when passed 0', () => { 26 | expect(polyfill(0)).to.equal(0) 27 | }) 28 | 29 | it('should return 0 when passed falsey values', () => { 30 | expect(polyfill(false)).to.equal(0) 31 | expect(polyfill('')).to.equal(0) 32 | expect(polyfill([])).to.equal(0) 33 | expect(polyfill(null)).to.equal(0) 34 | }) 35 | 36 | it('should return NaN when passed non-falsey non-numbers', () => { 37 | expect(polyfill('foo')).to.be.NaN 38 | expect(polyfill({})).to.be.NaN 39 | expect(polyfill([1, 2, 3])).to.be.NaN 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /test/sauce.conf.js: -------------------------------------------------------------------------------- 1 | let launchers = {} 2 | 3 | let mobileLaunchers = [ 4 | ['iOS', '10.3', 'Safari', 'iPhone 7 Simulator', '1.9.1'], 5 | ['iOS', '11.3', 'Safari', 'iPhone 7 Simulator', '1.9.1'], 6 | ['iOS', '12.2', 'Safari', 'iPhone 7 Simulator', '1.13.0'], 7 | ['iOS', '13.0', 'Safari', 'iPhone 7 Simulator', '1.15.0'], 8 | ['Android', '5.1', 'Browser', 'Android Emulator', '1.15.0'], 9 | ['Android', '6.0', 'Chrome', 'Android Emulator', '1.15.0'], 10 | ['Android', '8.0', 'Chrome', 'Android Emulator', '1.15.0'] 11 | ] 12 | 13 | for (let [platform, version, browser, device, appium] of mobileLaunchers) { 14 | let launcher = `sl_${platform}_${version}_${browser}` 15 | .replace(/[^a-z0-9]/gi, '_') 16 | .toLowerCase() 17 | 18 | launchers[launcher] = { 19 | name: `${browser}, ${platform} ${version}`, 20 | platformName: platform, 21 | platformVersion: version, 22 | browserName: browser, 23 | deviceName: device, 24 | deviceOrientation: 'portrait', 25 | appiumVersion: appium 26 | } 27 | } 28 | 29 | let desktopLaunchers = [ 30 | ['Windows 8.1', 'Internet Explorer', '11.0'], 31 | ['Windows 8', 'Internet Explorer', '10.0'], 32 | ['macOS 10.12', 'Safari', '11.0'], 33 | ['OS X 10.11', 'Safari', '10.0'], 34 | ['OS X 10.11', 'Safari', '9.0'] 35 | ] 36 | 37 | for (let [platform, browser, version] of desktopLaunchers) { 38 | let launcher = `sl_${platform}_${browser}_${version}` 39 | .replace(/[^a-z0-9]/gi, '_') 40 | .toLowerCase() 41 | 42 | launchers[launcher] = { 43 | name: `${browser} ${version}, ${platform}`, 44 | browserName: browser, 45 | version, 46 | platform 47 | } 48 | } 49 | 50 | for (let browser of ['Chrome', 'Firefox', 'MicrosoftEdge']) { 51 | let pastVersions = 3 52 | do { 53 | pastVersions-- 54 | let postfix = pastVersions > 0 ? `-${pastVersions}` : '' 55 | let version = 'latest' + postfix 56 | 57 | let browserName = browser === 'MicrosoftEdge' ? 'Edge' : browser 58 | let launcher = `sl_win10_${browser}_latest${postfix}`.replace(/-/g, '_').toLowerCase() 59 | 60 | launchers[launcher] = { 61 | name: `${browserName} ${version}, Windows 10`, 62 | browserName: browser, 63 | version, 64 | platform: 'Windows 10' 65 | } 66 | } while (pastVersions) 67 | } 68 | 69 | for (let launcher in launchers) { 70 | launchers[launcher].base = 'SauceLabs' 71 | } 72 | 73 | module.exports = launchers 74 | -------------------------------------------------------------------------------- /test/timeout.spec.js: -------------------------------------------------------------------------------- 1 | // describe('suite delay for DOM inspection', function () { 2 | // it('should delay by 10 minutes', function (done) { 3 | // document.documentElement.style = 'background-color: #eee; height: 100%' 4 | // const time = 1000 * 60 * 10 * 6 5 | // this.timeout(time) 6 | // setTimeout(done, time - 500) 7 | // }) 8 | // }) 9 | -------------------------------------------------------------------------------- /test/utils/deep-assign.spec.js: -------------------------------------------------------------------------------- 1 | import deepAssign from '../../src/utils/deep-assign' 2 | 3 | describe('Utilities', () => { 4 | describe('deepAssign()', () => { 5 | it('should assign source values to target object', () => { 6 | const target = { foo: 'bar', bun: 'baz' } 7 | const source = { foo: 'bonk!', bif: 'baff' } 8 | const goal = { foo: 'bonk!', bun: 'baz', bif: 'baff' } 9 | deepAssign(target, source) 10 | expect(target).to.deep.equal(goal) 11 | }) 12 | 13 | it('should assign nested source values to target object', () => { 14 | // each property tests a 15 | // different execution path 16 | const target = { 17 | foo: 'initial', 18 | bar: 'initial', 19 | kel: { pow: 'pop' }, 20 | zad: null 21 | } 22 | const source = { 23 | foo: 'bonk!', 24 | bar: { baz: 'baff' }, 25 | kel: { pow: 'lol' }, 26 | zad: { min: 'max' } 27 | } 28 | const goal = { 29 | foo: 'bonk!', 30 | bar: { baz: 'baff' }, 31 | kel: { pow: 'lol' }, 32 | zad: { min: 'max' } 33 | } 34 | deepAssign(target, source) 35 | expect(target).to.deep.equal(goal) 36 | }) 37 | 38 | it('should accept multiple sources', () => { 39 | const target = { foo: 'bar', bun: 'baz' } 40 | const source1 = { foo: 'bonk!', bif: 'baff' } 41 | const source2 = { foo: 'pow!' } 42 | const goal = { foo: 'pow!', bun: 'baz', bif: 'baff' } 43 | deepAssign(target, source1, source2) 44 | expect(target).to.deep.equal(goal) 45 | }) 46 | 47 | it('should throw a type error when not passed an object literal', () => { 48 | let caught 49 | try { 50 | deepAssign(null, null) 51 | } catch (error) { 52 | caught = error 53 | } 54 | expect(caught).to.exist 55 | expect(caught).to.be.an.instanceof(TypeError) 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /test/utils/each.spec.js: -------------------------------------------------------------------------------- 1 | import each from '../../src/utils/each' 2 | 3 | describe('Utilities', () => { 4 | describe('each()', () => { 5 | function Fixture() { 6 | this.foo = 'bar' 7 | this.baz = 'bun' 8 | } 9 | 10 | describe('if passed an object literal...', () => { 11 | it('should invoke callback for each property', () => { 12 | const fixture = new Fixture() 13 | const spy = sinon.spy() 14 | each(fixture, spy) 15 | expect(spy).to.have.been.calledTwice 16 | }) 17 | 18 | it('should ignore properties on the prototype chain', () => { 19 | Fixture.prototype.biff = 'baff' 20 | const fixture = new Fixture() 21 | const spy = sinon.spy() 22 | each(fixture, spy) 23 | expect(spy).to.have.been.calledTwice 24 | }) 25 | 26 | it('should pass the value, key and collection to the callback', () => { 27 | const fixture = new Fixture() 28 | let _value, _key, _collection 29 | each(fixture, (value, key, collection) => { 30 | _value = value 31 | _key = key 32 | _collection = collection 33 | }) 34 | expect(_value).to.equal('bun') 35 | expect(_key).to.equal('baz') 36 | expect(_collection).to.deep.equal(fixture) 37 | }) 38 | }) 39 | 40 | describe('if passed an array...', () => { 41 | const fixture = ['apple', 'orange', 'banana'] 42 | 43 | it('should invoke callback for each value', () => { 44 | const spy = sinon.spy() 45 | each(fixture, spy) 46 | expect(spy).to.have.been.calledThrice 47 | }) 48 | 49 | it('should pass the value, index and collection to the callback', () => { 50 | let _value, _index, _collection 51 | each(fixture, (value, index, collection) => { 52 | _value = value 53 | _index = index 54 | _collection = collection 55 | }) 56 | expect(_value).to.equal('banana') 57 | expect(_index).to.equal(2) 58 | expect(_collection).to.deep.equal(fixture) 59 | }) 60 | }) 61 | 62 | describe('else', () => { 63 | it('should throw a type error when passed an invalid collection', () => { 64 | let caught 65 | try { 66 | each(null, () => {}) 67 | } catch (error) { 68 | caught = error 69 | } 70 | expect(caught).to.exist 71 | expect(caught).to.be.an.instanceof(TypeError) 72 | }) 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /test/utils/get-prefixed-css-prop.spec.js: -------------------------------------------------------------------------------- 1 | import getPrefixedCssProp from '../../src/utils/get-prefixed-css-prop' 2 | 3 | describe('Utilities', () => { 4 | describe('getPrefixedCssProp()', () => { 5 | beforeEach('clear cache', () => { 6 | getPrefixedCssProp.clearCache() 7 | }) 8 | 9 | it('should return unprefixed properties before prefixed', () => { 10 | const source = { 11 | transform: '', 12 | '-webkit-transform': '' 13 | } 14 | const result = getPrefixedCssProp('transform', source) 15 | expect(result).to.equal('transform') 16 | }) 17 | 18 | it('should return prefixed property names', () => { 19 | const source = { '-webkit-transform': '' } 20 | const result = getPrefixedCssProp('transform', source) 21 | expect(result).to.equal('-webkit-transform') 22 | }) 23 | 24 | it('should return property names from cache when available', () => { 25 | const source = { '-webkit-transform': '' } 26 | getPrefixedCssProp('transform', source) 27 | const result = getPrefixedCssProp('transform', {}) 28 | expect(result).to.equal('-webkit-transform') 29 | }) 30 | 31 | it('should throw a range error when no property is found', () => { 32 | let caught 33 | try { 34 | getPrefixedCssProp('transform', {}) 35 | } catch (error) { 36 | caught = error 37 | } 38 | expect(caught).to.exist 39 | expect(caught).to.be.an.instanceof(RangeError) 40 | }) 41 | 42 | it('should throw a type error if not passed a string', () => { 43 | let caught 44 | try { 45 | getPrefixedCssProp(null) 46 | } catch (error) { 47 | caught = error 48 | } 49 | expect(caught).to.exist 50 | expect(caught).to.be.an.instanceof(TypeError) 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /test/utils/is-mobile.spec.js: -------------------------------------------------------------------------------- 1 | import isMobile from '../../src/utils/is-mobile' 2 | 3 | describe('Utilities', () => { 4 | describe('isMobile()', () => { 5 | it('should return true when passed a mobile user agent', () => { 6 | const android = `Mozilla/5.0 (Linux; U; Android 4.2; en-us; 7 | Android SDK built for x86 Build/JOP40C) AppleWebKit/534.30 8 | (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30` 9 | 10 | const iPhone = `Mozilla/5.0 (iPhone; CPU iPhone OS 10_10_5 like Mac OS X) 11 | AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B411 Safari/600.1.4` 12 | 13 | expect(isMobile(android)).to.be.true 14 | expect(isMobile(iPhone)).to.be.true 15 | }) 16 | 17 | it('should return false when passed a desktop user agent', () => { 18 | const chrome = `Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 19 | (KHTML, like Gecko) Chrome/50.0.2661.75 Safari/537.36` 20 | 21 | const firefox = 22 | 'Mozilla/5.0 (X11; Linux i686; rv:45.0) Gecko/20100101 Firefox/45.0' 23 | 24 | const ie10 = `Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; 25 | WOW64; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET4.0C; .NET4.0E)` 26 | 27 | expect(isMobile(chrome)).to.be.false 28 | expect(isMobile(firefox)).to.be.false 29 | expect(isMobile(ie10)).to.be.false 30 | }) 31 | 32 | it('should work when not passed an explicit user agent', () => { 33 | expect(isMobile()).to.be.a('boolean') 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /test/utils/is-object.spec.js: -------------------------------------------------------------------------------- 1 | import isObject from '../../src/utils/is-object' 2 | 3 | describe('Utilities', () => { 4 | describe('isObject()', () => { 5 | it('should return true when passed an object literal', () => { 6 | const result = isObject({}) 7 | expect(result).to.be.true 8 | }) 9 | 10 | it('should return false when passed a function', () => { 11 | const result = isObject(() => {}) 12 | expect(result).to.be.false 13 | }) 14 | 15 | it('should return false when passed an array', () => { 16 | const result = isObject([]) 17 | expect(result).to.be.false 18 | }) 19 | 20 | it('should return false when passed null', () => { 21 | const result = isObject(null) 22 | expect(result).to.be.false 23 | }) 24 | 25 | it('should return false when passed undefined', () => { 26 | const result = isObject(undefined) 27 | expect(result).to.be.false 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/utils/is-transform-supported.spec.js: -------------------------------------------------------------------------------- 1 | import isTransformSupported from '../../src/utils/is-transform-supported' 2 | 3 | describe('Utilities', () => { 4 | describe('isTransformSupported()', () => { 5 | it('should return true', () => { 6 | expect(isTransformSupported()).to.be.true 7 | }) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /test/utils/is-transition-supported.spec.js: -------------------------------------------------------------------------------- 1 | import isTransitionSupported from '../../src/utils/is-transition-supported' 2 | 3 | describe('Utilities', () => { 4 | describe('isTransitionSupported()', () => { 5 | it('should return true', () => { 6 | expect(isTransitionSupported()).to.be.true 7 | }) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /test/utils/logger.spec.js: -------------------------------------------------------------------------------- 1 | import logger from '../../src/utils/logger' 2 | 3 | describe('Utilities', () => { 4 | describe('logger()', () => { 5 | const mock = { constructor: { debug: true } } 6 | 7 | let spy 8 | let stub 9 | 10 | beforeEach('stub console log', () => { 11 | spy = sinon.spy() 12 | stub = sinon.stub(console, 'log').callsFake(spy) 13 | }) 14 | 15 | it('should invoke console.log', () => { 16 | logger.call(mock) 17 | expect(spy).to.have.been.called 18 | }) 19 | 20 | it('should prepend output with `ScrollReveal: `', () => { 21 | logger.call(mock, 'test') 22 | const result = '%cScrollReveal: test' 23 | const style = 'color: #ea654b;' 24 | expect(spy).to.have.been.calledWith(result, style) 25 | }) 26 | 27 | it('should accept multiple arguments as message details', () => { 28 | logger.call(mock, 'message', 'detail one', 'detail two') 29 | const result = '%cScrollReveal: message\n — detail one\n — detail two' 30 | const style = 'color: #ea654b;' 31 | expect(spy).to.have.been.calledWith(result, style) 32 | }) 33 | 34 | afterEach('restore console log', () => stub.restore()) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /test/utils/next-unique-id.spec.js: -------------------------------------------------------------------------------- 1 | import nextUniqueId from '../../src/utils/next-unique-id' 2 | 3 | describe('Utilities', () => { 4 | describe('nextUniqueId()', () => { 5 | it('should start at 0', () => { 6 | const result = nextUniqueId() 7 | expect(result).to.equal(0) 8 | }) 9 | 10 | it('should increment by 1', () => { 11 | const result = nextUniqueId() 12 | expect(result).to.equal(1) 13 | }) 14 | 15 | it('should return a number', () => { 16 | const result = nextUniqueId() 17 | expect(result).to.be.a('number') 18 | }) 19 | }) 20 | }) 21 | --------------------------------------------------------------------------------