├── .gitignore ├── LICENSE.txt ├── README.md ├── dist ├── scroll.observer.umd.js └── scroll.observer.umd.min.js ├── package-lock.json ├── package.json ├── rollup.config.js └── src └── scroll.observer.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.idea 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Martin Laxenaire 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lightweight vanilla javascript library to handle intersection observers 2 | 3 |

Initializing

4 | 5 | ```javascript 6 | import {ScrollObserver} from './src/scroll.observer.js'; 7 | 8 | const scrollObserver = new ScrollObserver(); 9 | ``` 10 | 11 | In a browser, you can use the UMD files located in the `dist` directory: 12 | 13 | ```html 14 | 15 | ``` 16 | 17 | 18 | ```javascript 19 | const scrollObserver = new ScrollObserver(); 20 | ``` 21 | 22 |

Parameters

23 | 24 |

Basic params

25 | 26 | The basic parameters you can specify are the same you'd use with an intersection observer (see https://developer.mozilla.org/fr/docs/Web/API/Intersection_Observer_API): 27 | 28 | | Parameter | Type | Default | Description | 29 | | --- | --- | --- | --- | 30 | | root | CSS selector | "viewport" | Root intersection observer parameter | 31 | | rootMargin | String | "0px" | Root margins to use | 32 | | threshold | array | 0 | Array of thresholds that will trigger the callback function | 33 | 34 | ```javascript 35 | const scrollObserver = new ScrollObserver({ 36 | // root 37 | root: document.getElementById("content"), 38 | // rootMargins 39 | rootMargin: "20px 0", 40 | // threshold array 41 | threshold: [0, 0.25, 0.5, 0.75, 1] 42 | }); 43 | ``` 44 | 45 |

Observe elements

46 | 47 | Once you've created an observer, you need to observe elements. 48 | 49 | You can add one or multiple elements to observe thanks to the `observe()` method: 50 | 51 | ```javascript 52 | scrollObserver.observe({ 53 | // elements to observe 54 | elements: document.querySelectorAll(".scroll-observed-el") 55 | }); 56 | ``` 57 | 58 | You can use a CSS selector instead of a list of elements: 59 | 60 | ```javascript 61 | // exactly the same as above 62 | scrollObserver.observe({ 63 | // elements to observe specified with a selector 64 | selector: ".scroll-observed-el" 65 | }); 66 | ``` 67 | 68 | There are 3 callbacks that you can use when observing an element, `onBeforeElVisible()`, `onElVisible()` and `onElHidden`, trigger by the intersection observer based on the `root`, `rootMargin` and `threshold` parameters: 69 | 70 | ```javascript 71 | scrollObserver.observe({ 72 | // elements to observe 73 | elements: document.querySelectorAll(".scroll-observed-el"), 74 | // wait for 200ms (cumulative) each time one of this element is visible 75 | stagger: 200, 76 | // an ".scroll-observed-el" element became visible 77 | onBeforeElVisible: (observedEl) => { 78 | // observedEl is an object containing the original HTML element, its bounding rectangle and other properties 79 | console.log(observedEl); 80 | }, 81 | // an ".scroll-observed-el" element became visible and its staggering timeout has run 82 | onElVisible: (observedEl) => { 83 | // observedEl is an object containing the original HTML element, its bounding rectangle and other properties 84 | console.log(observedEl); 85 | }, 86 | // an ".scroll-observed-el" element became hidden 87 | onElHidden: (observedEl) => { 88 | // observedEl is an object containing the original HTML element, its bounding rectangle and other properties 89 | console.log(observedEl); 90 | }, 91 | }); 92 | ``` 93 | 94 |

Complete parameter list

95 | 96 | Here is a complete list of all `observe()` method parameters: 97 | 98 | | Parameter | Type | Default | Description | 99 | | --- | --- | --- | --- | 100 | | elements | array of HTML elements | [] | elements to observe | 101 | | selector | string | null | elements to observe based on a CSS selector | 102 | | keepObserving | bool | false | Whether we should keep observing the element after it has been in and out of view | 103 | | triggerRatio | float between 0 and 1 | 0 | The ratio at which the visible/hidden callback should be called | 104 | | alwaysTrigger | bool | true | Whether it should trigger the visible callback multiple times or just once | 105 | | stagger | float | 0 | Number of milliseconds to wait before calling the visible callback (used for staggering animations) | 106 | | onBeforeElVisible | function(observedEl) | void | Function to execute right when this element become visible. Use it to change this element staggering property for example | 107 | | onElVisible | function(observedEl) | void | Function to execute when this element become visible, after the staggering timeout | 108 | | onElHidden | function(observedEl) | void | Function to execute when this element become hidden | 109 | 110 | 111 |

Unobserve elements

112 | 113 | You can decide to stop observing elements whenever you want. 114 | 115 |

Unobserving one element

116 | 117 | You can pass a HTML element to the `unobserveEl()` method: 118 | 119 | ```javascript 120 | scrollObserver.unobserveEl(document.getElementById("observed-el")); 121 | ``` 122 | 123 |

Unobserving multiple elements

124 | 125 | You can pass an array of elements to the `unobserveEls()` method: 126 | 127 | ```javascript 128 | scrollObserver.unobserveEls(document.querySelectorAll(".scroll-observed-el")); 129 | ``` 130 | 131 |

Unobserving all elements and disconnect observer

132 | 133 | You can also decide to unobserve all elements and disconnect your observer: 134 | 135 | ```javascript 136 | scrollObserver.unobserveAll(); 137 | ``` 138 | 139 | 140 |

Events

141 | 142 | There are 4 events you can use, `onError()`, `onBeforeElVisible()`, `onElVisible()` and `onElHidden()`: 143 | 144 | ```javascript 145 | scrollObserver.onError(() => { 146 | // intersection observer API is not supported, do something 147 | }).onBeforeElVisible((observedEl) => { 148 | // called each time an observed element become visible 149 | console.log(observedEl); 150 | }).onElVisible((observedEl) => { 151 | // called each time an observed element become visible but after its staggering timeout 152 | console.log(observedEl); 153 | }).onElHidden((observedEl) => { 154 | // called each time an observed element become hidden 155 | console.log(observedEl); 156 | }); 157 | ``` -------------------------------------------------------------------------------- /dist/scroll.observer.umd.js: -------------------------------------------------------------------------------- 1 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 2 | 3 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 4 | 5 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 6 | 7 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 8 | 9 | (function (global, factory) { 10 | (typeof exports === "undefined" ? "undefined" : _typeof(exports)) === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = global || self, factory(global.window = global.window || {})); 11 | })(this, function (exports) { 12 | 'use strict'; 13 | /*** 14 | Lightweight vanilla javascript library to handle intersection observers 15 | Inspired from past work & and Baptiste Briel work: http://awams.bbriel.me/ 16 | Author: Martin Laxenaire https://www.martin-laxenaire.fr/ 17 | Version: 1.2.0 18 | ***/ 19 | 20 | var ScrollObserver = /*#__PURE__*/function () { 21 | /*** 22 | If the intersection observer API is available, init our observer 23 | call the onError callback else 24 | params (see https://developer.mozilla.org/fr/docs/Web/API/Intersection_Observer_API): 25 | @root (CSS selector): root intersection observer params, viewport if null 26 | @rootMargin (string): root margins to use, default to 0 pixel 27 | @threshold (array): array of thresholds that will trigger the callback function, default to 0 28 | ***/ 29 | function ScrollObserver(_ref) { 30 | var _this = this; 31 | 32 | var root = _ref.root, 33 | _ref$rootMargin = _ref.rootMargin, 34 | rootMargin = _ref$rootMargin === void 0 ? "0px" : _ref$rootMargin, 35 | _ref$threshold = _ref.threshold, 36 | threshold = _ref$threshold === void 0 ? 0 : _ref$threshold; 37 | 38 | _classCallCheck(this, ScrollObserver); 39 | 40 | if (!!window.IntersectionObserver) { 41 | this.root = document.querySelector(root); 42 | 43 | if (!this.root) { 44 | this.root = "viewport"; 45 | } 46 | 47 | this.rootMargin = rootMargin; 48 | this.threshold = threshold; // cache elements to observe 49 | 50 | this.els = []; 51 | this._staggerEls = []; 52 | this.observer = new IntersectionObserver(this._callback.bind(this), { 53 | root: this.root === "viewport" ? null : this.root, 54 | rootMargin: this.rootMargin, 55 | threshold: this.threshold 56 | }); 57 | } else { 58 | // intersection observer API not supported, trigger onError callback 59 | setTimeout(function () { 60 | _this._onErrorCallback && _this._onErrorCallback(); 61 | }, 0); 62 | } 63 | } 64 | /*** 65 | This is our intersection observer callback 66 | called each time an entry crosses a threshold 67 | Can call callback functions based on element parameters 68 | ***/ 69 | 70 | 71 | _createClass(ScrollObserver, [{ 72 | key: "_callback", 73 | value: function _callback(entries) { 74 | var _this2 = this; 75 | 76 | entries.forEach(function (entry, index) { 77 | // find our entry in our cache elements array 78 | var cachedEl = _this2.els.find(function (data) { 79 | return data.el.isSameNode(entry.target); 80 | }); 81 | 82 | if (cachedEl) { 83 | // if intersection ratio is bigger than the triggerRatio property 84 | if (entry.intersectionRatio > cachedEl.triggerRatio) { 85 | // if we should always trigger it or if visibility hasn't been triggered yet 86 | if (cachedEl.alwaysTrigger || !cachedEl.inView) { 87 | // on before el visible callback 88 | cachedEl.onBeforeElVisible && cachedEl.onBeforeElVisible(cachedEl); 89 | _this2._onBeforeElVisibleCallback && _this2._onBeforeElVisibleCallback(cachedEl); // calculate staggering timeout 90 | 91 | var staggerTimeout = cachedEl.stagger; 92 | 93 | _this2._staggerEls.forEach(function (el) { 94 | if (el.inView) { 95 | staggerTimeout += el.stagger; 96 | } 97 | }); 98 | 99 | _this2._staggerEls.push(cachedEl); // apply staggering timeout 100 | 101 | 102 | setTimeout(function () { 103 | cachedEl.onElVisible && cachedEl.onElVisible(cachedEl); 104 | _this2._onElVisibleCallback && _this2._onElVisibleCallback(cachedEl); // remove from the staggering els array 105 | 106 | _this2._staggerEls = _this2._staggerEls.filter(function (el) { 107 | return cachedEl.id !== el.id; 108 | }); 109 | }, staggerTimeout); 110 | } // element is now in view 111 | 112 | 113 | cachedEl.inView = true; 114 | } else if (cachedEl.inView && entry.intersectionRatio <= cachedEl.triggerRatio) { 115 | // if intersection ratio is smaller than our trigger ratio and our element is visible 116 | // element is no more visible 117 | cachedEl.inView = false; 118 | cachedEl.onElHidden && cachedEl.onElHidden(cachedEl); 119 | _this2._onElHiddenCallback && _this2._onElHiddenCallback(cachedEl); // if we should observe it just once, unobserve it now 120 | 121 | if (!cachedEl.keepObserving) { 122 | _this2._unobserve(cachedEl); 123 | } 124 | } // update its ratio property 125 | 126 | 127 | cachedEl.ratio = entry.intersectionRatio; // update its boundingClientRect object 128 | 129 | cachedEl.boundingClientRect = entry.boundingClientRect; 130 | } 131 | }); 132 | } 133 | /*** 134 | Start observing a set of elements 135 | params: 136 | @elements (array of HTML elements): elements to observe 137 | @selector (CSS selector): elements to query based on a CSS selector 138 | @keepObserving (bool): whether we should keep observing the element after it has been in and out of view, default to false 139 | @triggerRatio (float between 0 and 1): the ratio at which the visible/hidden callback should be called, default to 0 140 | @alwaysTrigger (bool): whether it should trigger the visible callback multiple times or just once, default to true 141 | @stagger (int): number of milliseconds to wait before calling the visible callback (used for staggering animations), default to 0 142 | @onBeforeElVisible (function): function to execute right before an element onElVisible callback is called, used it to change its stagger property for example 143 | @onElVisible (function): function to execute when an element become visible 144 | @onElHidden (function): function to execute when an element become hidden 145 | ***/ 146 | 147 | }, { 148 | key: "observe", 149 | value: function observe(_ref2) { 150 | var _ref2$elements = _ref2.elements, 151 | elements = _ref2$elements === void 0 ? [] : _ref2$elements, 152 | selector = _ref2.selector, 153 | _ref2$keepObserving = _ref2.keepObserving, 154 | keepObserving = _ref2$keepObserving === void 0 ? false : _ref2$keepObserving, 155 | _ref2$triggerRatio = _ref2.triggerRatio, 156 | triggerRatio = _ref2$triggerRatio === void 0 ? 0 : _ref2$triggerRatio, 157 | _ref2$alwaysTrigger = _ref2.alwaysTrigger, 158 | alwaysTrigger = _ref2$alwaysTrigger === void 0 ? true : _ref2$alwaysTrigger, 159 | _ref2$stagger = _ref2.stagger, 160 | stagger = _ref2$stagger === void 0 ? 0 : _ref2$stagger, 161 | _ref2$onBeforeElVisib = _ref2.onBeforeElVisible, 162 | onBeforeElVisible = _ref2$onBeforeElVisib === void 0 ? function () {} : _ref2$onBeforeElVisib, 163 | _ref2$onElVisible = _ref2.onElVisible, 164 | onElVisible = _ref2$onElVisible === void 0 ? function () {} : _ref2$onElVisible, 165 | _ref2$onElHidden = _ref2.onElHidden, 166 | onElHidden = _ref2$onElHidden === void 0 ? function () {} : _ref2$onElHidden; 167 | var els = elements.length ? elements : document.querySelectorAll(selector); // add elements to our els array and start observing them 168 | 169 | for (var i = 0; i < els.length; i++) { 170 | this.els.push({ 171 | el: els[i], 172 | // HTML element 173 | keepObserving: keepObserving, 174 | triggerRatio: Math.max(0, Math.min(1, triggerRatio)) || 0, 175 | alwaysTrigger: alwaysTrigger, 176 | ratio: 0, 177 | stagger: stagger, 178 | onBeforeElVisible: onBeforeElVisible, 179 | onElVisible: onElVisible, 180 | onElHidden: onElHidden, 181 | inView: false, 182 | // not in view on init 183 | boundingClientRect: { 184 | x: 0, 185 | y: 0, 186 | width: 0, 187 | height: 0, 188 | top: 0, 189 | right: 0, 190 | bottom: 0, 191 | left: 0 192 | } 193 | }); // observe our HTML element 194 | 195 | this.observer.observe(els[i]); 196 | } 197 | } 198 | /*** 199 | Unobserve a cached element and remove it from our els array 200 | params: 201 | @element (object): an element of our els array 202 | ***/ 203 | 204 | }, { 205 | key: "_unobserve", 206 | value: function _unobserve(element) { 207 | // unobserve HTML element 208 | this.observer.unobserve(element.el); // remove element from our els array 209 | 210 | this.els = this.els.filter(function (data) { 211 | return !data.el.isSameNode(element.el); 212 | }); 213 | } 214 | /*** 215 | Unobserve a cached element by specifying its HTML element property 216 | params: 217 | @htmlElement (HTML element): a HTML element to stop watching 218 | ***/ 219 | 220 | }, { 221 | key: "unobserveEl", 222 | value: function unobserveEl(htmlElement) { 223 | // find our HTML element in our array els array and unobserve it 224 | var cachedEl = this.els.find(function (data) { 225 | return data.el.isSameNode(htmlElement); 226 | }); 227 | 228 | if (cachedEl) { 229 | this._unobserve(cachedEl); 230 | } 231 | } 232 | /*** 233 | Unobserve a cached element by specifying its HTML element property 234 | params: 235 | @htmlElements (HTML elements): an array of HTML elements to stop watching 236 | ***/ 237 | 238 | }, { 239 | key: "unobserveEls", 240 | value: function unobserveEls(htmlElements) { 241 | for (var i = 0; i < htmlElements.length; i++) { 242 | this.unobserveEl(htmlElements[i]); 243 | } 244 | } 245 | /*** 246 | Unobserve all elements 247 | ***/ 248 | 249 | }, { 250 | key: "unobserveAll", 251 | value: function unobserveAll() { 252 | this.observer.disconnect(); // reset our els array 253 | 254 | this.els = []; 255 | } 256 | /*** EVENTS ***/ 257 | 258 | /*** 259 | This is called if the intersection observer API not supported 260 | params: 261 | @callback (function): a function to execute 262 | returns: 263 | @this: our ScrollObserver element to handle chaining 264 | ***/ 265 | 266 | }, { 267 | key: "onError", 268 | value: function onError(callback) { 269 | if (callback) { 270 | this._onErrorCallback = callback; 271 | } 272 | 273 | return this; 274 | } 275 | /*** 276 | This is called when an element state switches from hidden to visible 277 | params: 278 | @callback (function): a function to execute 279 | returns: 280 | @this: our ScrollObserver element to handle chaining 281 | ***/ 282 | 283 | }, { 284 | key: "onBeforeElVisible", 285 | value: function onBeforeElVisible(callback) { 286 | if (callback) { 287 | this._onBeforeElVisibleCallback = callback; 288 | } 289 | 290 | return this; 291 | } 292 | /*** 293 | This is called when an element state switches from hidden to visible 294 | params: 295 | @callback (function): a function to execute 296 | returns: 297 | @this: our ScrollObserver element to handle chaining 298 | ***/ 299 | 300 | }, { 301 | key: "onElVisible", 302 | value: function onElVisible(callback) { 303 | if (callback) { 304 | this._onElVisibleCallback = callback; 305 | } 306 | 307 | return this; 308 | } 309 | /*** 310 | This is called when an element state switches from visible to hidden 311 | params: 312 | @callback (function): a function to execute 313 | returns: 314 | @this: our ScrollObserver element to handle chaining 315 | ***/ 316 | 317 | }, { 318 | key: "onElHidden", 319 | value: function onElHidden(callback) { 320 | if (callback) { 321 | this._onElHiddenCallback = callback; 322 | } 323 | 324 | return this; 325 | } 326 | }]); 327 | 328 | return ScrollObserver; 329 | }(); 330 | 331 | exports.ScrollObserver = ScrollObserver; 332 | Object.defineProperty(exports, '__esModule', { 333 | value: true 334 | }); 335 | }); 336 | -------------------------------------------------------------------------------- /dist/scroll.observer.umd.min.js: -------------------------------------------------------------------------------- 1 | function _classCallCheck(e,i){if(!(e instanceof i))throw new TypeError("Cannot call a class as a function")}function _defineProperties(e,i){for(var o=0;ot.triggerRatio){if(t.alwaysTrigger||!t.inView){t.onBeforeElVisible&&t.onBeforeElVisible(t),i._onBeforeElVisibleCallback&&i._onBeforeElVisibleCallback(t);var n=t.stagger;i._staggerEls.forEach((function(e){e.inView&&(n+=e.stagger)})),i._staggerEls.push(t),setTimeout((function(){t.onElVisible&&t.onElVisible(t),i._onElVisibleCallback&&i._onElVisibleCallback(t),i._staggerEls=i._staggerEls.filter((function(e){return t.id!==e.id}))}),n)}t.inView=!0}else t.inView&&e.intersectionRatio<=t.triggerRatio&&(t.inView=!1,t.onElHidden&&t.onElHidden(t),i._onElHiddenCallback&&i._onElHiddenCallback(t),t.keepObserving||i._unobserve(t));t.ratio=e.intersectionRatio,t.boundingClientRect=e.boundingClientRect}}))}},{key:"observe",value:function(e){for(var i=e.elements,o=void 0===i?[]:i,t=e.selector,n=e.keepObserving,r=void 0!==n&&n,l=e.triggerRatio,s=void 0===l?0:l,a=e.alwaysTrigger,u=void 0===a||a,c=e.stagger,f=void 0===c?0:c,b=e.onBeforeElVisible,d=void 0===b?function(){}:b,h=e.onElVisible,v=void 0===h?function(){}:h,g=e.onElHidden,y=void 0===g?function(){}:g,E=o.length?o:document.querySelectorAll(t),p=0;p { 49 | this._onErrorCallback && this._onErrorCallback(); 50 | }, 0); 51 | } 52 | } 53 | 54 | 55 | /*** 56 | This is our intersection observer callback 57 | called each time an entry crosses a threshold 58 | Can call callback functions based on element parameters 59 | ***/ 60 | _callback(entries) { 61 | entries.forEach((entry, index) => { 62 | // find our entry in our cache elements array 63 | const cachedEl = this.els.find(data => data.el.isSameNode(entry.target)); 64 | 65 | if(cachedEl) { 66 | // if intersection ratio is bigger than the triggerRatio property 67 | if(entry.intersectionRatio > cachedEl.triggerRatio) { 68 | // if we should always trigger it or if visibility hasn't been triggered yet 69 | if(cachedEl.alwaysTrigger || !cachedEl.inView) { 70 | // on before el visible callback 71 | cachedEl.onBeforeElVisible && cachedEl.onBeforeElVisible(cachedEl); 72 | this._onBeforeElVisibleCallback && this._onBeforeElVisibleCallback(cachedEl); 73 | 74 | // calculate staggering timeout 75 | let staggerTimeout = cachedEl.stagger; 76 | this._staggerEls.forEach(el => { 77 | if(el.inView) { 78 | staggerTimeout += el.stagger; 79 | } 80 | }); 81 | 82 | this._staggerEls.push(cachedEl); 83 | 84 | // apply staggering timeout 85 | setTimeout(() => { 86 | cachedEl.onElVisible && cachedEl.onElVisible(cachedEl); 87 | this._onElVisibleCallback && this._onElVisibleCallback(cachedEl); 88 | 89 | // remove from the staggering els array 90 | this._staggerEls = this._staggerEls.filter(el => cachedEl.id !== el.id); 91 | 92 | }, staggerTimeout); 93 | } 94 | 95 | // element is now in view 96 | cachedEl.inView = true; 97 | } 98 | else if(cachedEl.inView && entry.intersectionRatio <= cachedEl.triggerRatio) { 99 | // if intersection ratio is smaller than our trigger ratio and our element is visible 100 | 101 | // element is no more visible 102 | cachedEl.inView = false; 103 | cachedEl.onElHidden && cachedEl.onElHidden(cachedEl); 104 | this._onElHiddenCallback && this._onElHiddenCallback(cachedEl); 105 | 106 | // if we should observe it just once, unobserve it now 107 | if(!cachedEl.keepObserving) { 108 | this._unobserve(cachedEl); 109 | } 110 | } 111 | 112 | // update its ratio property 113 | cachedEl.ratio = entry.intersectionRatio; 114 | // update its boundingClientRect object 115 | cachedEl.boundingClientRect = entry.boundingClientRect; 116 | } 117 | }); 118 | } 119 | 120 | 121 | /*** 122 | Start observing a set of elements 123 | 124 | params: 125 | @elements (array of HTML elements): elements to observe 126 | @selector (CSS selector): elements to query based on a CSS selector 127 | @keepObserving (bool): whether we should keep observing the element after it has been in and out of view, default to false 128 | @triggerRatio (float between 0 and 1): the ratio at which the visible/hidden callback should be called, default to 0 129 | @alwaysTrigger (bool): whether it should trigger the visible callback multiple times or just once, default to true 130 | @stagger (int): number of milliseconds to wait before calling the visible callback (used for staggering animations), default to 0 131 | 132 | @onBeforeElVisible (function): function to execute right before an element onElVisible callback is called, used it to change its stagger property for example 133 | @onElVisible (function): function to execute when an element become visible 134 | @onElHidden (function): function to execute when an element become hidden 135 | ***/ 136 | observe({ 137 | elements = [], 138 | selector, 139 | keepObserving = false, 140 | triggerRatio = 0, 141 | alwaysTrigger = true, 142 | 143 | stagger = 0, 144 | 145 | onBeforeElVisible = () => {}, 146 | onElVisible = () => {}, 147 | onElHidden = () => {}, 148 | }) { 149 | const els = elements.length ? elements : document.querySelectorAll(selector); 150 | 151 | // add elements to our els array and start observing them 152 | for(let i = 0; i < els.length; i++) { 153 | this.els.push({ 154 | el: els[i], // HTML element 155 | keepObserving: keepObserving, 156 | triggerRatio: Math.max(0, Math.min(1, triggerRatio)) || 0, 157 | alwaysTrigger: alwaysTrigger, 158 | ratio: 0, 159 | stagger: stagger, 160 | onBeforeElVisible: onBeforeElVisible, 161 | onElVisible: onElVisible, 162 | onElHidden: onElHidden, 163 | inView: false, // not in view on init 164 | boundingClientRect: { 165 | x: 0, 166 | y: 0, 167 | width: 0, 168 | height: 0, 169 | top: 0, 170 | right: 0, 171 | bottom: 0, 172 | left: 0, 173 | }, 174 | }); 175 | // observe our HTML element 176 | this.observer.observe(els[i]); 177 | } 178 | } 179 | 180 | 181 | /*** 182 | Unobserve a cached element and remove it from our els array 183 | 184 | params: 185 | @element (object): an element of our els array 186 | ***/ 187 | _unobserve(element) { 188 | // unobserve HTML element 189 | this.observer.unobserve(element.el); 190 | 191 | // remove element from our els array 192 | this.els = this.els.filter(data => !data.el.isSameNode(element.el)); 193 | } 194 | 195 | 196 | /*** 197 | Unobserve a cached element by specifying its HTML element property 198 | 199 | params: 200 | @htmlElement (HTML element): a HTML element to stop watching 201 | ***/ 202 | unobserveEl(htmlElement) { 203 | // find our HTML element in our array els array and unobserve it 204 | const cachedEl = this.els.find(data => data.el.isSameNode(htmlElement)); 205 | if(cachedEl) { 206 | this._unobserve(cachedEl); 207 | } 208 | } 209 | 210 | 211 | /*** 212 | Unobserve a cached element by specifying its HTML element property 213 | 214 | params: 215 | @htmlElements (HTML elements): an array of HTML elements to stop watching 216 | ***/ 217 | unobserveEls(htmlElements) { 218 | for(let i = 0; i < htmlElements.length; i++) { 219 | this.unobserveEl(htmlElements[i]); 220 | } 221 | } 222 | 223 | 224 | /*** 225 | Unobserve all elements 226 | ***/ 227 | unobserveAll() { 228 | this.observer.disconnect(); 229 | 230 | // reset our els array 231 | this.els = []; 232 | } 233 | 234 | 235 | 236 | /*** EVENTS ***/ 237 | 238 | /*** 239 | This is called if the intersection observer API not supported 240 | 241 | params: 242 | @callback (function): a function to execute 243 | 244 | returns: 245 | @this: our ScrollObserver element to handle chaining 246 | ***/ 247 | onError(callback) { 248 | if(callback) { 249 | this._onErrorCallback = callback; 250 | } 251 | 252 | return this; 253 | } 254 | 255 | 256 | /*** 257 | This is called when an element state switches from hidden to visible 258 | 259 | params: 260 | @callback (function): a function to execute 261 | 262 | returns: 263 | @this: our ScrollObserver element to handle chaining 264 | ***/ 265 | onBeforeElVisible(callback) { 266 | if(callback) { 267 | this._onBeforeElVisibleCallback = callback; 268 | } 269 | 270 | return this; 271 | } 272 | 273 | 274 | /*** 275 | This is called when an element state switches from hidden to visible 276 | 277 | params: 278 | @callback (function): a function to execute 279 | 280 | returns: 281 | @this: our ScrollObserver element to handle chaining 282 | ***/ 283 | onElVisible(callback) { 284 | if(callback) { 285 | this._onElVisibleCallback = callback; 286 | } 287 | 288 | return this; 289 | } 290 | 291 | 292 | /*** 293 | This is called when an element state switches from visible to hidden 294 | 295 | params: 296 | @callback (function): a function to execute 297 | 298 | returns: 299 | @this: our ScrollObserver element to handle chaining 300 | ***/ 301 | onElHidden(callback) { 302 | if(callback) { 303 | this._onElHiddenCallback = callback; 304 | } 305 | 306 | return this; 307 | } 308 | } --------------------------------------------------------------------------------