├── .editorconfig ├── .gitignore ├── LICENSE.md ├── README.md ├── bower.json ├── dist ├── ScrollWatch-2.0.1.js ├── ScrollWatch-2.0.1.min.js └── ScrollWatch-2.0.1.min.js.map ├── gulp └── tasks │ ├── clean.js │ ├── default.js │ └── scripts.js ├── gulpfile.js ├── package.json └── src └── ScrollWatch.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [**.{js,json,html}] 14 | indent_style = tab 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### SASS Ignores - "Sassy CSS" http://sass-lang.com/ 2 | *.sass-cache 3 | 4 | 5 | # SublimeText project files 6 | *.sublime-workspace 7 | *.sublime-project 8 | 9 | # Node Modules 10 | node_modules/ 11 | 12 | # Vagrant 13 | .vagrant/ 14 | # https://docs.vagrantup.com/v2/synced-folders/virtualbox.html 15 | httpd.conf 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Evan Dull 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ScrollWatch 2 | =========== 3 | 4 | [https://edull24.github.io/ScrollWatch/](https://edull24.github.io/ScrollWatch/) 5 | 6 | The MIT License (MIT) 7 | 8 | Copyright (c) 2015 Evan Dull 9 | 10 | 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: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | 14 | 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. 15 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrollwatch", 3 | "version": "2.0.1", 4 | "description": "Easily add lazy loading, infinite scrolling, or any other dynamic interaction based on scroll position (with no dependencies).", 5 | "main": "./dist/ScrollWatch-2.0.1.min.js", 6 | "moduleType": ["globals", "amd", "node"], 7 | "authors": "Evan Dull ", 8 | "homepage": "https://edull24.github.io/ScrollWatch/", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/edull24/ScrollWatch.git" 13 | }, 14 | "ignore": [], 15 | "keywords": [ 16 | "javascript", 17 | "lazy load", 18 | "infinite scroll", 19 | "library", 20 | "scroll watch", 21 | "scroll" 22 | ], 23 | "devDependencies": { 24 | "del": "^1.1.1", 25 | "gulp": "^3.8.11", 26 | "gulp-header": "^1.2.2", 27 | "gulp-load-plugins": "^0.8.0", 28 | "gulp-rename": "^1.2.0", 29 | "gulp-rev": "^3.0.1", 30 | "gulp-sourcemaps": "^1.3.0", 31 | "gulp-uglify": "^1.1.0", 32 | "gulp-umd": "^0.1.3", 33 | "gulp-util": "^3.0.3", 34 | "require-dir": "^0.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /dist/ScrollWatch-2.0.1.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define([], factory); 4 | } else if (typeof exports === 'object') { 5 | module.exports = factory(); 6 | } else { 7 | root.ScrollWatch = factory(); 8 | } 9 | }(this, function() { 10 | 'use strict'; 11 | 12 | // Give each instance on the page a unique ID 13 | var instanceId = 0; 14 | 15 | // Store instance data privately so it can't be accessed/modified 16 | var instanceData = {}; 17 | 18 | var config = { 19 | // The default container is window, but we need the actual documentElement to determine positioning. 20 | container: window.document.documentElement, 21 | watch: '[data-scroll-watch]', 22 | watchOnce: true, 23 | inViewClass: 'scroll-watch-in-view', 24 | ignoreClass: 'scroll-watch-ignore', 25 | debounce: false, 26 | debounceTriggerLeading: false, 27 | scrollDebounce: 250, 28 | resizeDebounce: 250, 29 | scrollThrottle: 250, 30 | resizeThrottle: 250, 31 | watchOffsetXLeft: 0, 32 | watchOffsetXRight: 0, 33 | watchOffsetYTop: 0, 34 | watchOffsetYBottom: 0, 35 | infiniteScroll: false, 36 | infiniteOffset: 0, 37 | onElementInView: function(){}, 38 | onElementOutOfView: function(){}, 39 | onInfiniteXInView: function(){}, 40 | onInfiniteYInView: function(){} 41 | }; 42 | 43 | var initEvent = 'scrollwatchinit'; 44 | 45 | var extend = function(retObj) { 46 | 47 | var len = arguments.length; 48 | var i; 49 | var key; 50 | var obj; 51 | 52 | retObj = retObj || {}; 53 | 54 | for (i = 1; i < len; i++) { 55 | 56 | obj = arguments[i]; 57 | 58 | if (!obj) { 59 | 60 | continue; 61 | 62 | } 63 | 64 | for (key in obj) { 65 | 66 | if (obj.hasOwnProperty(key)) { 67 | 68 | retObj[key] = obj[key]; 69 | 70 | } 71 | 72 | } 73 | } 74 | 75 | return retObj; 76 | 77 | }; 78 | 79 | var throttle = function (fn, threshhold, scope) { 80 | 81 | var last; 82 | var deferTimer; 83 | 84 | threshhold = threshhold || 250; 85 | 86 | return function () { 87 | 88 | var context = scope || this; 89 | var now = +new Date(); 90 | var args = arguments; 91 | 92 | if (last && now < last + threshhold) { 93 | 94 | window.clearTimeout(deferTimer); 95 | 96 | deferTimer = setTimeout(function () { 97 | 98 | last = now; 99 | 100 | fn.apply(context, args); 101 | 102 | }, threshhold); 103 | 104 | } else { 105 | 106 | last = now; 107 | 108 | fn.apply(context, args); 109 | 110 | } 111 | 112 | }; 113 | 114 | }; 115 | 116 | // http://underscorejs.org/#debounce 117 | var debounce = function(func, wait, immediate) { 118 | 119 | var timeout; 120 | var args; 121 | var context; 122 | var timestamp; 123 | var result; 124 | 125 | var later = function() { 126 | 127 | var last = new Date().getTime() - timestamp; 128 | 129 | if (last < wait && last >= 0) { 130 | 131 | timeout = setTimeout(later, wait - last); 132 | 133 | } else { 134 | 135 | timeout = null; 136 | 137 | if (!immediate) { 138 | 139 | result = func.apply(context, args); 140 | 141 | if (!timeout) { 142 | 143 | context = args = null; 144 | 145 | } 146 | 147 | } 148 | 149 | } 150 | 151 | }; 152 | 153 | return function() { 154 | 155 | var callNow = immediate && !timeout; 156 | 157 | context = this; 158 | args = arguments; 159 | timestamp = new Date().getTime(); 160 | 161 | if (!timeout) { 162 | 163 | timeout = setTimeout(later, wait); 164 | 165 | } 166 | 167 | if (callNow) { 168 | 169 | result = func.apply(context, args); 170 | context = args = null; 171 | 172 | } 173 | 174 | return result; 175 | 176 | }; 177 | 178 | }; 179 | 180 | // If a string was passed in as the container element, use it as a selector and query the DOM, otherwise we'll assume a DOM node was passed in 181 | var saveContainerElement = function() { 182 | 183 | var config = instanceData[this._id].config; 184 | 185 | if (typeof config.container === 'string') { 186 | 187 | // A selector was passed in for the container 188 | config.container = document.querySelector(config.container); 189 | 190 | } 191 | 192 | }; 193 | 194 | // Save all elements to watch into an array 195 | var saveElements = function() { 196 | 197 | instanceData[this._id].elements = Array.prototype.slice.call(document.querySelectorAll(instanceData[this._id].config.watch + ':not(.' + instanceData[this._id].config.ignoreClass + ')')); 198 | 199 | }; 200 | 201 | // Save the scroll position of the scrolling container so we can perform comparison checks 202 | var saveScrollPosition = function() { 203 | 204 | instanceData[this._id].lastScrollPosition = getScrollPosition.call(this); 205 | 206 | }; 207 | 208 | var checkViewport = function(eventType) { 209 | 210 | checkElements.call(this, eventType); 211 | checkInfinite.call(this, eventType); 212 | 213 | // Chrome does not return 0,0 for scroll position when reloading a page that was previously scrolled. To combat this, we will leave the scroll position at the default 0,0 when a page is first loaded. 214 | if (eventType !== initEvent) { 215 | 216 | saveScrollPosition.call(this); 217 | 218 | } 219 | 220 | }; 221 | 222 | // Determine if the watched elements are viewable within the scrolling container 223 | var checkElements = function(eventType) { 224 | 225 | var data = instanceData[this._id]; 226 | var len = data.elements.length; 227 | var config = data.config; 228 | var inViewClass = config.inViewClass; 229 | var responseData = { 230 | eventType: eventType 231 | }; 232 | var el; 233 | var i; 234 | 235 | for (i = 0; i < len; i++) { 236 | 237 | el = data.elements[i]; 238 | 239 | // Prepare the data to pass to the callback 240 | responseData.el = el; 241 | 242 | if (eventType === 'scroll') { 243 | 244 | responseData.direction = getScrolledDirection.call(this, getScrolledAxis.call(this)); 245 | 246 | } 247 | 248 | if (isElementInView.call(this, el)) { 249 | 250 | if (!el.classList.contains(inViewClass)) { 251 | 252 | // Add a class hook and fire a callback for every element that just came into view 253 | 254 | el.classList.add(inViewClass); 255 | config.onElementInView.call(this, responseData); 256 | 257 | if (config.watchOnce) { 258 | 259 | // Remove this element so we don't check it again next time 260 | 261 | data.elements.splice(i, 1); 262 | len--; 263 | i--; 264 | 265 | // Flag this element with the ignore class so we don't store it again if a refresh happens 266 | 267 | el.classList.add(config.ignoreClass); 268 | 269 | } 270 | 271 | } 272 | 273 | } else { 274 | 275 | if (el.classList.contains(inViewClass) || eventType === initEvent) { 276 | 277 | // Remove the class hook and fire a callback for every element that just went out of view 278 | 279 | el.classList.remove(inViewClass); 280 | config.onElementOutOfView.call(this, responseData); 281 | 282 | } 283 | 284 | } 285 | 286 | } 287 | 288 | }; 289 | 290 | // Determine if the infinite scroll zone is in view. This could come into view by scrolling or resizing. Initial load must also be accounted for. 291 | var checkInfinite = function(eventType) { 292 | 293 | var data = instanceData[this._id]; 294 | var config = data.config; 295 | var i; 296 | var axis; 297 | var container; 298 | var viewableRange; 299 | var scrollSize; 300 | var callback; 301 | var responseData; 302 | 303 | if (config.infiniteScroll && !data.isInfiniteScrollPaused) { 304 | 305 | axis = ['x', 'y']; 306 | callback = ['onInfiniteXInView', 'onInfiniteYInView']; 307 | container = config.container; 308 | viewableRange = getViewableRange.call(this); 309 | scrollSize = [container.scrollWidth, container.scrollHeight]; 310 | responseData = {}; 311 | 312 | for (i = 0; i < 2; i++) { 313 | 314 | // If a scroll event triggered this check, verify the scroll position actually changed for each axis. This stops horizontal scrolls from triggering infiniteY callbacks and vice versa. In other words, only trigger an infinite callback if that axis was actually scrolled. 315 | 316 | if ((eventType === 'scroll' && hasScrollPositionChanged.call(this, axis[i]) || eventType === 'resize'|| eventType === 'refresh' || eventType === initEvent) && viewableRange[axis[i]].end + config.infiniteOffset >= scrollSize[i]) { 317 | 318 | // We've scrolled/resized all the way to the right/bottom 319 | 320 | responseData.eventType = eventType; 321 | 322 | if (eventType === 'scroll') { 323 | 324 | responseData.direction = getScrolledDirection.call(this, axis[i]); 325 | 326 | } 327 | 328 | config[callback[i]].call(this, responseData); 329 | 330 | } 331 | 332 | } 333 | 334 | } 335 | 336 | }; 337 | 338 | // Add listeners to the scrolling container for each instance 339 | var addListeners = function() { 340 | 341 | var data = instanceData[this._id]; 342 | var scrollingElement = getScrollingElement.call(this); 343 | 344 | scrollingElement.addEventListener('scroll', data.scrollHandler, false); 345 | scrollingElement.addEventListener('resize', data.resizeHandler, false); 346 | 347 | }; 348 | 349 | var removeListeners = function() { 350 | 351 | var data = instanceData[this._id]; 352 | var scrollingElement = getScrollingElement.call(this); 353 | 354 | scrollingElement.removeEventListener('scroll', data.scrollHandler); 355 | scrollingElement.removeEventListener('resize', data.resizeHandler); 356 | 357 | }; 358 | 359 | var getScrollingElement = function() { 360 | 361 | return isContainerWindow.call(this) ? window : instanceData[this._id].config.container; 362 | 363 | }; 364 | 365 | // Get the width and height of viewport/scrolling container 366 | var getViewportSize = function() { 367 | 368 | var size = { 369 | w: instanceData[this._id].config.container.clientWidth, 370 | h: instanceData[this._id].config.container.clientHeight 371 | }; 372 | 373 | return size; 374 | 375 | }; 376 | 377 | // Get the scrollbar position of the scrolling container 378 | var getScrollPosition = function() { 379 | 380 | var pos = {}; 381 | var container; 382 | 383 | if (isContainerWindow.call(this)) { 384 | 385 | pos.left = window.pageXOffset; 386 | pos.top = window.pageYOffset; 387 | 388 | 389 | } else { 390 | 391 | container = instanceData[this._id].config.container; 392 | 393 | pos.left = container.scrollLeft; 394 | pos.top = container.scrollTop; 395 | 396 | } 397 | 398 | return pos; 399 | 400 | }; 401 | 402 | // Get the pixel range currently viewable within the scrolling container 403 | var getViewableRange = function() { 404 | 405 | var range = { 406 | x: {}, 407 | y: {} 408 | }; 409 | var scrollPos = getScrollPosition.call(this); 410 | var viewportSize = getViewportSize.call(this); 411 | 412 | range.x.start = scrollPos.left; 413 | range.x.end = range.x.start + viewportSize.w; 414 | range.x.size = range.x.end - range.x.start; 415 | 416 | range.y.start = scrollPos.top; 417 | range.y.end = range.y.start + viewportSize.h; 418 | range.y.size = range.y.end - range.y.start; 419 | 420 | return range; 421 | 422 | }; 423 | 424 | // Get the pixel range of where this element falls within the scrolling container 425 | var getElementRange = function(el) { 426 | 427 | var range = { 428 | x: {}, 429 | y: {} 430 | }; 431 | var viewableRange = getViewableRange.call(this); 432 | var coords = el.getBoundingClientRect(); 433 | var containerCoords; 434 | 435 | if (isContainerWindow.call(this)) { 436 | 437 | range.x.start = coords.left + viewableRange.x.start; 438 | range.x.end = coords.right + viewableRange.x.start; 439 | 440 | 441 | range.y.start = coords.top + viewableRange.y.start; 442 | range.y.end = coords.bottom + viewableRange.y.start; 443 | 444 | } else { 445 | 446 | containerCoords = instanceData[this._id].config.container.getBoundingClientRect(); 447 | 448 | range.x.start = (coords.left - containerCoords.left) + viewableRange.x.start; 449 | range.x.end = range.x.start + coords.width; 450 | 451 | range.y.start = (coords.top - containerCoords.top) + viewableRange.y.start; 452 | range.y.end = range.y.start + coords.height; 453 | 454 | } 455 | 456 | range.x.size = range.x.end - range.x.start; 457 | range.y.size = range.y.end - range.y.start; 458 | 459 | return range; 460 | 461 | }; 462 | 463 | // Determines which axis was just scrolled (x/horizontal or y/vertical) 464 | var getScrolledAxis = function() { 465 | 466 | if (hasScrollPositionChanged.call(this, 'x')) { 467 | 468 | return 'x'; 469 | 470 | } 471 | 472 | if (hasScrollPositionChanged.call(this, 'y')) { 473 | 474 | return 'y'; 475 | 476 | } 477 | 478 | }; 479 | 480 | var getScrolledDirection = function(axis) { 481 | 482 | var scrollDir = {x: ['right', 'left'], y: ['down', 'up']}; 483 | var position = {x: 'left', y: 'top'}; 484 | var lastScrollPosition = instanceData[this._id].lastScrollPosition; 485 | var curScrollPosition = getScrollPosition.call(this); 486 | 487 | return curScrollPosition[position[axis]] > lastScrollPosition[position[axis]] ? scrollDir[axis][0] : scrollDir[axis][1]; 488 | 489 | }; 490 | 491 | var hasScrollPositionChanged = function(axis) { 492 | 493 | var position = {x: 'left', y: 'top'}; 494 | var lastScrollPosition = instanceData[this._id].lastScrollPosition; 495 | var curScrollPosition = getScrollPosition.call(this); 496 | 497 | return curScrollPosition[position[axis]] !== lastScrollPosition[position[axis]]; 498 | 499 | }; 500 | 501 | var isElementInView = function(el) { 502 | 503 | var viewableRange = getViewableRange.call(this); 504 | var elRange = getElementRange.call(this, el); 505 | 506 | return isElementInVerticalView.call(this, elRange, viewableRange) && isElementInHorizontalView.call(this, elRange, viewableRange); 507 | 508 | }; 509 | 510 | var isElementInVerticalView = function(elRange, viewableRange) { 511 | 512 | var config = instanceData[this._id].config; 513 | 514 | return elRange.y.start < viewableRange.y.end + config.watchOffsetYBottom && elRange.y.end > viewableRange.y.start - config.watchOffsetYTop; 515 | 516 | }; 517 | 518 | var isElementInHorizontalView = function(elRange, viewableRange) { 519 | 520 | var config = instanceData[this._id].config; 521 | 522 | return elRange.x.start < viewableRange.x.end + config.watchOffsetXRight && elRange.x.end > viewableRange.x.start - config.watchOffsetXLeft; 523 | 524 | }; 525 | 526 | var isContainerWindow = function() { 527 | 528 | return instanceData[this._id].config.container === window.document.documentElement; 529 | 530 | }; 531 | 532 | var mergeOptions = function(opts) { 533 | 534 | extend(instanceData[this._id].config, config, opts); 535 | 536 | }; 537 | 538 | var handler = function(e) { 539 | 540 | var eventType = e.type; 541 | 542 | // Protect against the instance being destroyed while we still have queued or pending handler events (via @jsonk000) 543 | if (!instanceData[this._id]) { 544 | 545 | return; 546 | 547 | } 548 | 549 | // For scroll events, only check the viewport if something has changed. Fixes issues when using gestures on a page that doesn't need to scroll. An event would still fire, but the position didn't change because the window/container "bounced" back into place. 550 | if (eventType === 'resize' || hasScrollPositionChanged.call(this, 'x') || hasScrollPositionChanged.call(this, 'y')) { 551 | 552 | checkViewport.call(this, eventType); 553 | 554 | } 555 | 556 | }; 557 | 558 | var ScrollWatch = function(opts) { 559 | 560 | var data; 561 | 562 | // Protect against missing new keyword 563 | if (this instanceof ScrollWatch) { 564 | 565 | Object.defineProperty(this, '_id', {value: instanceId++}); 566 | 567 | // Keep all instance data private, except for the '_id', which will be the key to get the private data for a specific instance 568 | 569 | data = instanceData[this._id] = { 570 | 571 | config: {}, 572 | // The elements to watch for this instance 573 | elements: [], 574 | lastScrollPosition: {top: 0, left: 0}, 575 | isInfiniteScrollPaused: false 576 | 577 | }; 578 | 579 | mergeOptions.call(this, opts); 580 | 581 | // In order to remove listeners later and keep a correct reference to 'this', give each instance it's own event handler 582 | if (data.config.debounce) { 583 | 584 | data.scrollHandler = debounce(handler.bind(this), data.config.scrollDebounce, data.config.debounceTriggerLeading); 585 | data.resizeHandler = debounce(handler.bind(this), data.config.resizeDebounce, data.config.debounceTriggerLeading); 586 | 587 | } else { 588 | 589 | data.scrollHandler = throttle(handler.bind(this), data.config.scrollThrottle, this); 590 | data.resizeHandler = throttle(handler.bind(this), data.config.resizeThrottle, this); 591 | 592 | } 593 | 594 | saveContainerElement.call(this); 595 | addListeners.call(this); 596 | saveElements.call(this); 597 | checkViewport.call(this, initEvent); 598 | 599 | } else { 600 | 601 | return new ScrollWatch(opts); 602 | 603 | } 604 | 605 | }; 606 | 607 | ScrollWatch.prototype = { 608 | 609 | // Should be manually called by user after loading in new content 610 | refresh: function() { 611 | 612 | saveElements.call(this); 613 | checkViewport.call(this, 'refresh'); 614 | 615 | }, 616 | 617 | destroy: function() { 618 | 619 | removeListeners.call(this); 620 | delete instanceData[this._id]; 621 | 622 | }, 623 | 624 | updateWatchOffsetXLeft: function(offset) { 625 | 626 | instanceData[this._id].config.watchOffsetXLeft = offset; 627 | 628 | }, 629 | 630 | updateWatchOffsetXRight: function(offset) { 631 | 632 | instanceData[this._id].config.watchOffsetXRight = offset; 633 | 634 | }, 635 | 636 | updateWatchOffsetYTop: function(offset) { 637 | 638 | instanceData[this._id].config.watchOffsetYTop = offset; 639 | 640 | }, 641 | 642 | updateWatchOffsetYBottom: function(offset) { 643 | 644 | instanceData[this._id].config.watchOffsetYBottom = offset; 645 | 646 | }, 647 | 648 | pauseInfiniteScroll: function() { 649 | 650 | instanceData[this._id].isInfiniteScrollPaused = true; 651 | 652 | }, 653 | 654 | resumeInfiniteScroll: function() { 655 | 656 | instanceData[this._id].isInfiniteScrollPaused = false; 657 | 658 | } 659 | 660 | }; 661 | 662 | return ScrollWatch; 663 | })); 664 | -------------------------------------------------------------------------------- /dist/ScrollWatch-2.0.1.min.js: -------------------------------------------------------------------------------- 1 | /*! scrollwatch v2.0.1 | (c) Mon Jan 01 2018 14:27:45 GMT-0500 (EST) Evan Dull | License: MIT | https://github.com/edull24/ScrollWatch.git*/ 2 | !function(t,i){"function"==typeof define&&define.amd?define([],i):"object"==typeof exports?module.exports=i():t.ScrollWatch=i()}(this,function(){"use strict";var t=0,i={},e={container:window.document.documentElement,watch:"[data-scroll-watch]",watchOnce:!0,inViewClass:"scroll-watch-in-view",ignoreClass:"scroll-watch-ignore",debounce:!1,debounceTriggerLeading:!1,scrollDebounce:250,resizeDebounce:250,scrollThrottle:250,resizeThrottle:250,watchOffsetXLeft:0,watchOffsetXRight:0,watchOffsetYTop:0,watchOffsetYBottom:0,infiniteScroll:!1,infiniteOffset:0,onElementInView:function(){},onElementOutOfView:function(){},onInfiniteXInView:function(){},onInfiniteYInView:function(){}},n="scrollwatchinit",s=function(t){var i,e,n,s=arguments.length;for(t=t||{},i=1;s>i;i++)if(n=arguments[i])for(e in n)n.hasOwnProperty(e)&&(t[e]=n[e]);return t},l=function(t,i,e){var n,s;return i=i||250,function(){var l=e||this,o=+new Date,c=arguments;n&&n+i>o?(window.clearTimeout(s),s=setTimeout(function(){n=o,t.apply(l,c)},i)):(n=o,t.apply(l,c))}},o=function(t,i,e){var n,s,l,o,c,r=function(){var a=(new Date).getTime()-o;i>a&&a>=0?n=setTimeout(r,i-a):(n=null,e||(c=t.apply(l,s),n||(l=s=null)))};return function(){var a=e&&!n;return l=this,s=arguments,o=(new Date).getTime(),n||(n=setTimeout(r,i)),a&&(c=t.apply(l,s),l=s=null),c}},c=function(){var t=i[this._id].config;"string"==typeof t.container&&(t.container=document.querySelector(t.container))},r=function(){i[this._id].elements=Array.prototype.slice.call(document.querySelectorAll(i[this._id].config.watch+":not(."+i[this._id].config.ignoreClass+")"))},a=function(){i[this._id].lastScrollPosition=p.call(this)},f=function(t){h.call(this,t),d.call(this,t),t!==n&&a.call(this)},h=function(t){var e,s,l=i[this._id],o=l.elements.length,c=l.config,r=c.inViewClass,a={eventType:t};for(s=0;o>s;s++)e=l.elements[s],a.el=e,"scroll"===t&&(a.direction=_.call(this,x.call(this))),T.call(this,e)?e.classList.contains(r)||(e.classList.add(r),c.onElementInView.call(this,a),c.watchOnce&&(l.elements.splice(s,1),o--,s--,e.classList.add(c.ignoreClass))):(e.classList.contains(r)||t===n)&&(e.classList.remove(r),c.onElementOutOfView.call(this,a))},d=function(t){var e,s,l,o,c,r,a,f=i[this._id],h=f.config;if(h.infiniteScroll&&!f.isInfiniteScrollPaused)for(s=["x","y"],r=["onInfiniteXInView","onInfiniteYInView"],l=h.container,o=m.call(this),c=[l.scrollWidth,l.scrollHeight],a={},e=0;2>e;e++)("scroll"===t&&O.call(this,s[e])||"resize"===t||"refresh"===t||t===n)&&o[s[e]].end+h.infiniteOffset>=c[e]&&(a.eventType=t,"scroll"===t&&(a.direction=_.call(this,s[e])),h[r[e]].call(this,a))},u=function(){var t=i[this._id],e=w.call(this);e.addEventListener("scroll",t.scrollHandler,!1),e.addEventListener("resize",t.resizeHandler,!1)},g=function(){var t=i[this._id],e=w.call(this);e.removeEventListener("scroll",t.scrollHandler),e.removeEventListener("resize",t.resizeHandler)},w=function(){return z.call(this)?window:i[this._id].config.container},y=function(){var t={w:i[this._id].config.container.clientWidth,h:i[this._id].config.container.clientHeight};return t},p=function(){var t,e={};return z.call(this)?(e.left=window.pageXOffset,e.top=window.pageYOffset):(t=i[this._id].config.container,e.left=t.scrollLeft,e.top=t.scrollTop),e},m=function(){var t={x:{},y:{}},i=p.call(this),e=y.call(this);return t.x.start=i.left,t.x.end=t.x.start+e.w,t.x.size=t.x.end-t.x.start,t.y.start=i.top,t.y.end=t.y.start+e.h,t.y.size=t.y.end-t.y.start,t},v=function(t){var e,n={x:{},y:{}},s=m.call(this),l=t.getBoundingClientRect();return z.call(this)?(n.x.start=l.left+s.x.start,n.x.end=l.right+s.x.start,n.y.start=l.top+s.y.start,n.y.end=l.bottom+s.y.start):(e=i[this._id].config.container.getBoundingClientRect(),n.x.start=l.left-e.left+s.x.start,n.x.end=n.x.start+l.width,n.y.start=l.top-e.top+s.y.start,n.y.end=n.y.start+l.height),n.x.size=n.x.end-n.x.start,n.y.size=n.y.end-n.y.start,n},x=function(){return O.call(this,"x")?"x":O.call(this,"y")?"y":void 0},_=function(t){var e={x:["right","left"],y:["down","up"]},n={x:"left",y:"top"},s=i[this._id].lastScrollPosition,l=p.call(this);return l[n[t]]>s[n[t]]?e[t][0]:e[t][1]},O=function(t){var e={x:"left",y:"top"},n=i[this._id].lastScrollPosition,s=p.call(this);return s[e[t]]!==n[e[t]]},T=function(t){var i=m.call(this),e=v.call(this,t);return L.call(this,e,i)&&b.call(this,e,i)},L=function(t,e){var n=i[this._id].config;return t.y.starte.y.start-n.watchOffsetYTop},b=function(t,e){var n=i[this._id].config;return t.x.starte.x.start-n.watchOffsetXLeft},z=function(){return i[this._id].config.container===window.document.documentElement},I=function(t){s(i[this._id].config,e,t)},S=function(t){var e=t.type;i[this._id]&&("resize"===e||O.call(this,"x")||O.call(this,"y"))&&f.call(this,e)},X=function(e){var s;return this instanceof X?(Object.defineProperty(this,"_id",{value:t++}),s=i[this._id]={config:{},elements:[],lastScrollPosition:{top:0,left:0},isInfiniteScrollPaused:!1},I.call(this,e),s.config.debounce?(s.scrollHandler=o(S.bind(this),s.config.scrollDebounce,s.config.debounceTriggerLeading),s.resizeHandler=o(S.bind(this),s.config.resizeDebounce,s.config.debounceTriggerLeading)):(s.scrollHandler=l(S.bind(this),s.config.scrollThrottle,this),s.resizeHandler=l(S.bind(this),s.config.resizeThrottle,this)),c.call(this),u.call(this),r.call(this),f.call(this,n),void 0):new X(e)};return X.prototype={refresh:function(){r.call(this),f.call(this,"refresh")},destroy:function(){g.call(this),delete i[this._id]},updateWatchOffsetXLeft:function(t){i[this._id].config.watchOffsetXLeft=t},updateWatchOffsetXRight:function(t){i[this._id].config.watchOffsetXRight=t},updateWatchOffsetYTop:function(t){i[this._id].config.watchOffsetYTop=t},updateWatchOffsetYBottom:function(t){i[this._id].config.watchOffsetYBottom=t},pauseInfiniteScroll:function(){i[this._id].isInfiniteScrollPaused=!0},resumeInfiniteScroll:function(){i[this._id].isInfiniteScrollPaused=!1}},X}); 3 | //# sourceMappingURL=ScrollWatch-2.0.1.min.js.map -------------------------------------------------------------------------------- /dist/ScrollWatch-2.0.1.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["ScrollWatch-2.0.1.min.js"],"names":["root","factory","define","amd","exports","module","ScrollWatch","this","instanceId","instanceData","config","container","window","document","documentElement","watch","watchOnce","inViewClass","ignoreClass","debounce","debounceTriggerLeading","scrollDebounce","resizeDebounce","scrollThrottle","resizeThrottle","watchOffsetXLeft","watchOffsetXRight","watchOffsetYTop","watchOffsetYBottom","infiniteScroll","infiniteOffset","onElementInView","onElementOutOfView","onInfiniteXInView","onInfiniteYInView","initEvent","extend","retObj","i","key","obj","len","arguments","length","hasOwnProperty","throttle","fn","threshhold","scope","last","deferTimer","context","now","Date","args","clearTimeout","setTimeout","apply","func","wait","immediate","timeout","timestamp","result","later","getTime","callNow","saveContainerElement","_id","querySelector","saveElements","elements","Array","prototype","slice","call","querySelectorAll","saveScrollPosition","lastScrollPosition","getScrollPosition","checkViewport","eventType","checkElements","checkInfinite","el","data","responseData","direction","getScrolledDirection","getScrolledAxis","isElementInView","classList","contains","add","splice","remove","axis","viewableRange","scrollSize","callback","isInfiniteScrollPaused","getViewableRange","scrollWidth","scrollHeight","hasScrollPositionChanged","end","addListeners","scrollingElement","getScrollingElement","addEventListener","scrollHandler","resizeHandler","removeListeners","removeEventListener","isContainerWindow","getViewportSize","size","w","clientWidth","h","clientHeight","pos","left","pageXOffset","top","pageYOffset","scrollLeft","scrollTop","range","x","y","scrollPos","viewportSize","start","getElementRange","containerCoords","coords","getBoundingClientRect","right","bottom","width","height","scrollDir","position","curScrollPosition","elRange","isElementInVerticalView","isElementInHorizontalView","mergeOptions","opts","handler","e","type","Object","defineProperty","value","bind","refresh","destroy","updateWatchOffsetXLeft","offset","updateWatchOffsetXRight","updateWatchOffsetYTop","updateWatchOffsetYBottom","pauseInfiniteScroll","resumeInfiniteScroll"],"mappings":"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","file":"ScrollWatch-2.0.1.min.js","sourcesContent":["(function(root, factory) {\n if (typeof define === 'function' && define.amd) {\n define([], factory);\n } else if (typeof exports === 'object') {\n module.exports = factory();\n } else {\n root.ScrollWatch = factory();\n }\n}(this, function() {\n'use strict';\n\n// Give each instance on the page a unique ID\nvar instanceId = 0;\n\n// Store instance data privately so it can't be accessed/modified\nvar instanceData = {};\n\nvar config = {\n\t// The default container is window, but we need the actual documentElement to determine positioning.\n\tcontainer: window.document.documentElement,\n\twatch: '[data-scroll-watch]',\n\twatchOnce: true,\n\tinViewClass: 'scroll-watch-in-view',\n\tignoreClass: 'scroll-watch-ignore',\n\tdebounce: false,\n\tdebounceTriggerLeading: false,\n\tscrollDebounce: 250,\n\tresizeDebounce: 250,\n\tscrollThrottle: 250,\n\tresizeThrottle: 250,\n\twatchOffsetXLeft: 0,\n\twatchOffsetXRight: 0,\n\twatchOffsetYTop: 0,\n\twatchOffsetYBottom: 0,\n\tinfiniteScroll: false,\n\tinfiniteOffset: 0,\n\tonElementInView: function(){},\n\tonElementOutOfView: function(){},\n\tonInfiniteXInView: function(){},\n\tonInfiniteYInView: function(){}\n};\n\nvar initEvent = 'scrollwatchinit';\n\nvar extend = function(retObj) {\n\n\tvar len = arguments.length;\n\tvar i;\n\tvar key;\n\tvar obj;\n\n\tretObj = retObj || {};\n\n\tfor (i = 1; i < len; i++) {\n\n\t\tobj = arguments[i];\n\n\t\tif (!obj) {\n\n\t\t\tcontinue;\n\n\t\t}\n\n\t\tfor (key in obj) {\n\n\t\t\tif (obj.hasOwnProperty(key)) {\n\n\t\t\t\tretObj[key] = obj[key];\n\n\t\t\t}\n\n\t\t}\n\t}\n\n\treturn retObj;\n\n};\n\nvar throttle = function (fn, threshhold, scope) {\n\n\tvar last;\n\tvar deferTimer;\n\n\tthreshhold = threshhold || 250;\n\n\treturn function () {\n\n\t\tvar context = scope || this;\n\t\tvar now = +new Date();\n\t\tvar args = arguments;\n\n\t\tif (last && now < last + threshhold) {\n\n\t\t\twindow.clearTimeout(deferTimer);\n\n\t\t\tdeferTimer = setTimeout(function () {\n\n\t\t\t\tlast = now;\n\n\t\t\t\tfn.apply(context, args);\n\n\t\t\t}, threshhold);\n\n\t\t} else {\n\n\t\t\tlast = now;\n\n\t\t\tfn.apply(context, args);\n\n\t\t}\n\n\t};\n\n};\n\n// http://underscorejs.org/#debounce\nvar debounce = function(func, wait, immediate) {\n\n\tvar timeout;\n\tvar args;\n\tvar context;\n\tvar timestamp;\n\tvar result;\n\n\tvar later = function() {\n\n\t\tvar last = new Date().getTime() - timestamp;\n\n\t\tif (last < wait && last >= 0) {\n\n\t\t\ttimeout = setTimeout(later, wait - last);\n\n\t\t} else {\n\n\t\t\ttimeout = null;\n\n\t\t\tif (!immediate) {\n\n\t\t\t\tresult = func.apply(context, args);\n\n\t\t\t\tif (!timeout) {\n\n\t\t\t\t\tcontext = args = null;\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\n\t};\n\n\treturn function() {\n\n\t\tvar callNow = immediate && !timeout;\n\n\t\tcontext = this;\n\t\targs = arguments;\n\t\ttimestamp = new Date().getTime();\n\n\t\tif (!timeout) {\n\n\t\t\ttimeout = setTimeout(later, wait);\n\n\t\t}\n\n\t\tif (callNow) {\n\n\t\t\tresult = func.apply(context, args);\n\t\t\tcontext = args = null;\n\n\t\t}\n\n\t\treturn result;\n\n\t};\n\n};\n\n// If a string was passed in as the container element, use it as a selector and query the DOM, otherwise we'll assume a DOM node was passed in\nvar saveContainerElement = function() {\n\n\tvar config = instanceData[this._id].config;\n\n\tif (typeof config.container === 'string') {\n\n\t\t// A selector was passed in for the container\n\t\tconfig.container = document.querySelector(config.container);\n\n\t}\n\n};\n\n// Save all elements to watch into an array\nvar saveElements = function() {\n\n\tinstanceData[this._id].elements = Array.prototype.slice.call(document.querySelectorAll(instanceData[this._id].config.watch + ':not(.' + instanceData[this._id].config.ignoreClass + ')'));\n\n};\n\n// Save the scroll position of the scrolling container so we can perform comparison checks\nvar saveScrollPosition = function() {\n\n\tinstanceData[this._id].lastScrollPosition = getScrollPosition.call(this);\n\n};\n\nvar checkViewport = function(eventType) {\n\n\tcheckElements.call(this, eventType);\n\tcheckInfinite.call(this, eventType);\n\n\t// Chrome does not return 0,0 for scroll position when reloading a page that was previously scrolled. To combat this, we will leave the scroll position at the default 0,0 when a page is first loaded.\n\tif (eventType !== initEvent) {\n\n\t\tsaveScrollPosition.call(this);\n\n\t}\n\n};\n\n// Determine if the watched elements are viewable within the scrolling container\nvar checkElements = function(eventType) {\n\n\tvar data = instanceData[this._id];\n\tvar len = data.elements.length;\n\tvar config = data.config;\n\tvar inViewClass = config.inViewClass;\n\tvar responseData = {\n\t\teventType: eventType\n\t};\n\tvar el;\n\tvar i;\n\n\tfor (i = 0; i < len; i++) {\n\n\t\tel = data.elements[i];\n\n\t\t// Prepare the data to pass to the callback\n\t\tresponseData.el = el;\n\n\t\tif (eventType === 'scroll') {\n\n\t\t\tresponseData.direction = getScrolledDirection.call(this, getScrolledAxis.call(this));\n\n\t\t}\n\n\t\tif (isElementInView.call(this, el)) {\n\n\t\t\tif (!el.classList.contains(inViewClass)) {\n\n\t\t\t\t// Add a class hook and fire a callback for every element that just came into view\n\n\t\t\t\tel.classList.add(inViewClass);\n\t\t\t\tconfig.onElementInView.call(this, responseData);\n\n\t\t\t\tif (config.watchOnce) {\n\n\t\t\t\t\t// Remove this element so we don't check it again next time\n\n\t\t\t\t\tdata.elements.splice(i, 1);\n\t\t\t\t\tlen--;\n\t\t\t\t\ti--;\n\n\t\t\t\t\t// Flag this element with the ignore class so we don't store it again if a refresh happens\n\n\t\t\t\t\tel.classList.add(config.ignoreClass);\n\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t} else {\n\n\t\t\tif (el.classList.contains(inViewClass) || eventType === initEvent) {\n\n\t\t\t\t// Remove the class hook and fire a callback for every element that just went out of view\n\n\t\t\t\tel.classList.remove(inViewClass);\n\t\t\t\tconfig.onElementOutOfView.call(this, responseData);\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n};\n\n// Determine if the infinite scroll zone is in view. This could come into view by scrolling or resizing. Initial load must also be accounted for.\nvar checkInfinite = function(eventType) {\n\n\tvar data = instanceData[this._id];\n\tvar config = data.config;\n\tvar i;\n\tvar axis;\n\tvar container;\n\tvar viewableRange;\n\tvar scrollSize;\n\tvar callback;\n\tvar responseData;\n\n\tif (config.infiniteScroll && !data.isInfiniteScrollPaused) {\n\n\t\taxis = ['x', 'y'];\n\t\tcallback = ['onInfiniteXInView', 'onInfiniteYInView'];\n\t\tcontainer = config.container;\n\t\tviewableRange = getViewableRange.call(this);\n\t\tscrollSize = [container.scrollWidth, container.scrollHeight];\n\t\tresponseData = {};\n\n\t\tfor (i = 0; i < 2; i++) {\n\n\t\t\t// If a scroll event triggered this check, verify the scroll position actually changed for each axis. This stops horizontal scrolls from triggering infiniteY callbacks and vice versa. In other words, only trigger an infinite callback if that axis was actually scrolled.\n\n\t\t\tif ((eventType === 'scroll' && hasScrollPositionChanged.call(this, axis[i]) || eventType === 'resize'|| eventType === 'refresh' || eventType === initEvent) && viewableRange[axis[i]].end + config.infiniteOffset >= scrollSize[i]) {\n\n\t\t\t\t// We've scrolled/resized all the way to the right/bottom\n\n\t\t\t\tresponseData.eventType = eventType;\n\n\t\t\t\tif (eventType === 'scroll') {\n\n\t\t\t\t\tresponseData.direction = getScrolledDirection.call(this, axis[i]);\n\n\t\t\t\t}\n\n\t\t\t\tconfig[callback[i]].call(this, responseData);\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n};\n\n// Add listeners to the scrolling container for each instance\nvar addListeners = function() {\n\n\tvar data = instanceData[this._id];\n\tvar scrollingElement = getScrollingElement.call(this);\n\n\tscrollingElement.addEventListener('scroll', data.scrollHandler, false);\n\tscrollingElement.addEventListener('resize', data.resizeHandler, false);\n\n};\n\nvar removeListeners = function() {\n\n\tvar data = instanceData[this._id];\n\tvar scrollingElement = getScrollingElement.call(this);\n\n\tscrollingElement.removeEventListener('scroll', data.scrollHandler);\n\tscrollingElement.removeEventListener('resize', data.resizeHandler);\n\n};\n\nvar getScrollingElement = function() {\n\n\treturn isContainerWindow.call(this) ? window : instanceData[this._id].config.container;\n\n};\n\n// Get the width and height of viewport/scrolling container\nvar getViewportSize = function() {\n\n\tvar size = {\n\t\tw: instanceData[this._id].config.container.clientWidth,\n\t\th: instanceData[this._id].config.container.clientHeight\n\t};\n\n\treturn size;\n\n};\n\n// Get the scrollbar position of the scrolling container\nvar getScrollPosition = function() {\n\n\tvar pos = {};\n\tvar container;\n\n\tif (isContainerWindow.call(this)) {\n\n\t\tpos.left = window.pageXOffset;\n\t\tpos.top = window.pageYOffset;\n\n\n\t} else {\n\n\t\tcontainer = instanceData[this._id].config.container;\n\n\t\tpos.left = container.scrollLeft;\n\t\tpos.top = container.scrollTop;\n\n\t}\n\n\treturn pos;\n\n};\n\n// Get the pixel range currently viewable within the scrolling container\nvar getViewableRange = function() {\n\n\tvar range = {\n\t\tx: {},\n\t\ty: {}\n\t};\n\tvar scrollPos = getScrollPosition.call(this);\n\tvar viewportSize = getViewportSize.call(this);\n\n\trange.x.start = scrollPos.left;\n\trange.x.end = range.x.start + viewportSize.w;\n\trange.x.size = range.x.end - range.x.start;\n\n\trange.y.start = scrollPos.top;\n\trange.y.end = range.y.start + viewportSize.h;\n\trange.y.size = range.y.end - range.y.start;\n\n\treturn range;\n\n};\n\n// Get the pixel range of where this element falls within the scrolling container\nvar getElementRange = function(el) {\n\n\tvar range = {\n\t\tx: {},\n\t\ty: {}\n\t};\n\tvar viewableRange = getViewableRange.call(this);\n\tvar coords = el.getBoundingClientRect();\n\tvar containerCoords;\n\n\tif (isContainerWindow.call(this)) {\n\n\t\trange.x.start = coords.left + viewableRange.x.start;\n\t\trange.x.end = coords.right + viewableRange.x.start;\n\n\n\t\trange.y.start = coords.top + viewableRange.y.start;\n\t\trange.y.end = coords.bottom + viewableRange.y.start;\n\n\t} else {\n\n\t\tcontainerCoords = instanceData[this._id].config.container.getBoundingClientRect();\n\n\t\trange.x.start = (coords.left - containerCoords.left) + viewableRange.x.start;\n\t\trange.x.end = range.x.start + coords.width;\n\n\t\trange.y.start = (coords.top - containerCoords.top) + viewableRange.y.start;\n\t\trange.y.end = range.y.start + coords.height;\n\n\t}\n\n\trange.x.size = range.x.end - range.x.start;\n\trange.y.size = range.y.end - range.y.start;\n\n\treturn range;\n\n};\n\n// Determines which axis was just scrolled (x/horizontal or y/vertical)\nvar getScrolledAxis = function() {\n\n\tif (hasScrollPositionChanged.call(this, 'x')) {\n\n\t\treturn 'x';\n\n\t}\n\n\tif (hasScrollPositionChanged.call(this, 'y')) {\n\n\t\treturn 'y';\n\n\t}\n\n};\n\nvar getScrolledDirection = function(axis) {\n\n\tvar scrollDir = {x: ['right', 'left'], y: ['down', 'up']};\n\tvar position = {x: 'left', y: 'top'};\n\tvar lastScrollPosition = instanceData[this._id].lastScrollPosition;\n\tvar curScrollPosition = getScrollPosition.call(this);\n\n\treturn curScrollPosition[position[axis]] > lastScrollPosition[position[axis]] ? scrollDir[axis][0] : scrollDir[axis][1];\n\n};\n\nvar hasScrollPositionChanged = function(axis) {\n\n\tvar position = {x: 'left', y: 'top'};\n\tvar lastScrollPosition = instanceData[this._id].lastScrollPosition;\n\tvar curScrollPosition = getScrollPosition.call(this);\n\n\treturn curScrollPosition[position[axis]] !== lastScrollPosition[position[axis]];\n\n};\n\nvar isElementInView = function(el) {\n\n\tvar viewableRange = getViewableRange.call(this);\n\tvar elRange = getElementRange.call(this, el);\n\n\treturn isElementInVerticalView.call(this, elRange, viewableRange) && isElementInHorizontalView.call(this, elRange, viewableRange);\n\n};\n\nvar isElementInVerticalView = function(elRange, viewableRange) {\n\n\tvar config = instanceData[this._id].config;\n\n\treturn elRange.y.start < viewableRange.y.end + config.watchOffsetYBottom && elRange.y.end > viewableRange.y.start - config.watchOffsetYTop;\n\n};\n\nvar isElementInHorizontalView = function(elRange, viewableRange) {\n\n\tvar config = instanceData[this._id].config;\n\n\treturn elRange.x.start < viewableRange.x.end + config.watchOffsetXRight && elRange.x.end > viewableRange.x.start - config.watchOffsetXLeft;\n\n};\n\nvar isContainerWindow = function() {\n\n\treturn instanceData[this._id].config.container === window.document.documentElement;\n\n};\n\nvar mergeOptions = function(opts) {\n\n\textend(instanceData[this._id].config, config, opts);\n\n};\n\nvar handler = function(e) {\n\n\tvar eventType = e.type;\n\n\t// Protect against the instance being destroyed while we still have queued or pending handler events (via @jsonk000)\n\tif (!instanceData[this._id]) {\n\n\t\treturn;\n\n\t}\n\n\t// For scroll events, only check the viewport if something has changed. Fixes issues when using gestures on a page that doesn't need to scroll. An event would still fire, but the position didn't change because the window/container \"bounced\" back into place.\n\tif (eventType === 'resize' || hasScrollPositionChanged.call(this, 'x') || hasScrollPositionChanged.call(this, 'y')) {\n\n\t\tcheckViewport.call(this, eventType);\n\n\t}\n\n};\n\nvar ScrollWatch = function(opts) {\n\n\tvar data;\n\n\t// Protect against missing new keyword\n\tif (this instanceof ScrollWatch) {\n\n\t\tObject.defineProperty(this, '_id', {value: instanceId++});\n\n\t\t// Keep all instance data private, except for the '_id', which will be the key to get the private data for a specific instance\n\n\t\tdata = instanceData[this._id] = {\n\n\t\t\tconfig: {},\n\t\t\t// The elements to watch for this instance\n\t\t\telements: [],\n\t\t\tlastScrollPosition: {top: 0, left: 0},\n\t\t\tisInfiniteScrollPaused: false\n\n\t\t};\n\n\t\tmergeOptions.call(this, opts);\n\n\t\t// In order to remove listeners later and keep a correct reference to 'this', give each instance it's own event handler\n\t\tif (data.config.debounce) {\n\n\t\t\tdata.scrollHandler = debounce(handler.bind(this), data.config.scrollDebounce, data.config.debounceTriggerLeading);\n\t\t\tdata.resizeHandler = debounce(handler.bind(this), data.config.resizeDebounce, data.config.debounceTriggerLeading);\n\n\t\t} else {\n\n\t\t\tdata.scrollHandler = throttle(handler.bind(this), data.config.scrollThrottle, this);\n\t\t\tdata.resizeHandler = throttle(handler.bind(this), data.config.resizeThrottle, this);\n\n\t\t}\n\n\t\tsaveContainerElement.call(this);\n\t\taddListeners.call(this);\n\t\tsaveElements.call(this);\n\t\tcheckViewport.call(this, initEvent);\n\n\t} else {\n\n\t\treturn new ScrollWatch(opts);\n\n\t}\n\n};\n\nScrollWatch.prototype = {\n\n\t// Should be manually called by user after loading in new content\n\trefresh: function() {\n\n\t\tsaveElements.call(this);\n\t\tcheckViewport.call(this, 'refresh');\n\n\t},\n\n\tdestroy: function() {\n\n\t\tremoveListeners.call(this);\n\t\tdelete instanceData[this._id];\n\n\t},\n\n\tupdateWatchOffsetXLeft: function(offset) {\n\n\t\tinstanceData[this._id].config.watchOffsetXLeft = offset;\n\n\t},\n\n\tupdateWatchOffsetXRight: function(offset) {\n\n\t\tinstanceData[this._id].config.watchOffsetXRight = offset;\n\n\t},\n\n\tupdateWatchOffsetYTop: function(offset) {\n\n\t\tinstanceData[this._id].config.watchOffsetYTop = offset;\n\n\t},\n\n\tupdateWatchOffsetYBottom: function(offset) {\n\n\t\tinstanceData[this._id].config.watchOffsetYBottom = offset;\n\n\t},\n\n\tpauseInfiniteScroll: function() {\n\n\t\tinstanceData[this._id].isInfiniteScrollPaused = true;\n\n\t},\n\n\tresumeInfiniteScroll: function() {\n\n\t\tinstanceData[this._id].isInfiniteScrollPaused = false;\n\n\t}\n\n};\n\nreturn ScrollWatch;\n}));\n"],"sourceRoot":"/source/"} -------------------------------------------------------------------------------- /gulp/tasks/clean.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var del = require('del'); 5 | 6 | gulp.task('clean', function(cb) { 7 | 8 | del([ 9 | './dist/**/*' 10 | ], cb); 11 | 12 | }); 13 | -------------------------------------------------------------------------------- /gulp/tasks/default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | gulp.task('default', ['scripts']); 6 | -------------------------------------------------------------------------------- /gulp/tasks/scripts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var plugins = require('gulp-load-plugins')(); 5 | var pkg = require('../../package.json'); 6 | 7 | gulp.task('scripts', ['clean'], function() { 8 | 9 | return gulp.src('./src/*.js') 10 | .pipe(plugins.umd()) 11 | .pipe(plugins.rename({extname: '-' + pkg.version + '.js'})) 12 | .pipe(gulp.dest('./dist')) 13 | .pipe(plugins.rename({extname: '.min.js'})) 14 | .pipe(plugins.sourcemaps.init()) 15 | .pipe(plugins.uglify()) 16 | .pipe(plugins.header('/*! ' + pkg.name + ' v' + pkg.version + ' | (c) ' + (new Date().toString()) + ' ' + pkg.author + ' | License: ' + pkg.license + ' | ' + pkg.repository.url + '*/\n')) 17 | .pipe(plugins.sourcemaps.write('./')) 18 | .pipe(gulp.dest('./dist')); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('require-dir')('./gulp/tasks'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrollwatch", 3 | "version": "2.0.1", 4 | "author": "Evan Dull ", 5 | "description": "Easily add lazy loading, infinite scrolling, or any other dynamic interaction based on scroll position (with no dependencies).", 6 | "main": "./dist/ScrollWatch-2.0.1.min.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/edull24/ScrollWatch.git" 10 | }, 11 | "scripts": { 12 | "gulp": "gulp" 13 | }, 14 | "keywords": [ 15 | "javascript", 16 | "lazy load", 17 | "infinite scroll", 18 | "library", 19 | "scroll watch", 20 | "scroll" 21 | ], 22 | "devDependencies": { 23 | "del": "^1.1.1", 24 | "gulp": "^3.8.11", 25 | "gulp-header": "^1.2.2", 26 | "gulp-load-plugins": "^0.8.0", 27 | "gulp-rename": "^1.2.0", 28 | "gulp-rev": "^3.0.1", 29 | "gulp-sourcemaps": "^1.3.0", 30 | "gulp-uglify": "^1.1.0", 31 | "gulp-umd": "^0.1.3", 32 | "gulp-util": "^3.0.3", 33 | "require-dir": "^0.1.0" 34 | }, 35 | "license": "MIT" 36 | } 37 | -------------------------------------------------------------------------------- /src/ScrollWatch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Give each instance on the page a unique ID 4 | var instanceId = 0; 5 | 6 | // Store instance data privately so it can't be accessed/modified 7 | var instanceData = {}; 8 | 9 | var config = { 10 | // The default container is window, but we need the actual documentElement to determine positioning. 11 | container: window.document.documentElement, 12 | watch: '[data-scroll-watch]', 13 | watchOnce: true, 14 | inViewClass: 'scroll-watch-in-view', 15 | ignoreClass: 'scroll-watch-ignore', 16 | debounce: false, 17 | debounceTriggerLeading: false, 18 | scrollDebounce: 250, 19 | resizeDebounce: 250, 20 | scrollThrottle: 250, 21 | resizeThrottle: 250, 22 | watchOffsetXLeft: 0, 23 | watchOffsetXRight: 0, 24 | watchOffsetYTop: 0, 25 | watchOffsetYBottom: 0, 26 | infiniteScroll: false, 27 | infiniteOffset: 0, 28 | onElementInView: function(){}, 29 | onElementOutOfView: function(){}, 30 | onInfiniteXInView: function(){}, 31 | onInfiniteYInView: function(){} 32 | }; 33 | 34 | var initEvent = 'scrollwatchinit'; 35 | 36 | var extend = function(retObj) { 37 | 38 | var len = arguments.length; 39 | var i; 40 | var key; 41 | var obj; 42 | 43 | retObj = retObj || {}; 44 | 45 | for (i = 1; i < len; i++) { 46 | 47 | obj = arguments[i]; 48 | 49 | if (!obj) { 50 | 51 | continue; 52 | 53 | } 54 | 55 | for (key in obj) { 56 | 57 | if (obj.hasOwnProperty(key)) { 58 | 59 | retObj[key] = obj[key]; 60 | 61 | } 62 | 63 | } 64 | } 65 | 66 | return retObj; 67 | 68 | }; 69 | 70 | var throttle = function (fn, threshhold, scope) { 71 | 72 | var last; 73 | var deferTimer; 74 | 75 | threshhold = threshhold || 250; 76 | 77 | return function () { 78 | 79 | var context = scope || this; 80 | var now = +new Date(); 81 | var args = arguments; 82 | 83 | if (last && now < last + threshhold) { 84 | 85 | window.clearTimeout(deferTimer); 86 | 87 | deferTimer = setTimeout(function () { 88 | 89 | last = now; 90 | 91 | fn.apply(context, args); 92 | 93 | }, threshhold); 94 | 95 | } else { 96 | 97 | last = now; 98 | 99 | fn.apply(context, args); 100 | 101 | } 102 | 103 | }; 104 | 105 | }; 106 | 107 | // http://underscorejs.org/#debounce 108 | var debounce = function(func, wait, immediate) { 109 | 110 | var timeout; 111 | var args; 112 | var context; 113 | var timestamp; 114 | var result; 115 | 116 | var later = function() { 117 | 118 | var last = new Date().getTime() - timestamp; 119 | 120 | if (last < wait && last >= 0) { 121 | 122 | timeout = setTimeout(later, wait - last); 123 | 124 | } else { 125 | 126 | timeout = null; 127 | 128 | if (!immediate) { 129 | 130 | result = func.apply(context, args); 131 | 132 | if (!timeout) { 133 | 134 | context = args = null; 135 | 136 | } 137 | 138 | } 139 | 140 | } 141 | 142 | }; 143 | 144 | return function() { 145 | 146 | var callNow = immediate && !timeout; 147 | 148 | context = this; 149 | args = arguments; 150 | timestamp = new Date().getTime(); 151 | 152 | if (!timeout) { 153 | 154 | timeout = setTimeout(later, wait); 155 | 156 | } 157 | 158 | if (callNow) { 159 | 160 | result = func.apply(context, args); 161 | context = args = null; 162 | 163 | } 164 | 165 | return result; 166 | 167 | }; 168 | 169 | }; 170 | 171 | // If a string was passed in as the container element, use it as a selector and query the DOM, otherwise we'll assume a DOM node was passed in 172 | var saveContainerElement = function() { 173 | 174 | var config = instanceData[this._id].config; 175 | 176 | if (typeof config.container === 'string') { 177 | 178 | // A selector was passed in for the container 179 | config.container = document.querySelector(config.container); 180 | 181 | } 182 | 183 | }; 184 | 185 | // Save all elements to watch into an array 186 | var saveElements = function() { 187 | 188 | instanceData[this._id].elements = Array.prototype.slice.call(document.querySelectorAll(instanceData[this._id].config.watch + ':not(.' + instanceData[this._id].config.ignoreClass + ')')); 189 | 190 | }; 191 | 192 | // Save the scroll position of the scrolling container so we can perform comparison checks 193 | var saveScrollPosition = function() { 194 | 195 | instanceData[this._id].lastScrollPosition = getScrollPosition.call(this); 196 | 197 | }; 198 | 199 | var checkViewport = function(eventType) { 200 | 201 | checkElements.call(this, eventType); 202 | checkInfinite.call(this, eventType); 203 | 204 | // Chrome does not return 0,0 for scroll position when reloading a page that was previously scrolled. To combat this, we will leave the scroll position at the default 0,0 when a page is first loaded. 205 | if (eventType !== initEvent) { 206 | 207 | saveScrollPosition.call(this); 208 | 209 | } 210 | 211 | }; 212 | 213 | // Determine if the watched elements are viewable within the scrolling container 214 | var checkElements = function(eventType) { 215 | 216 | var data = instanceData[this._id]; 217 | var len = data.elements.length; 218 | var config = data.config; 219 | var inViewClass = config.inViewClass; 220 | var responseData = { 221 | eventType: eventType 222 | }; 223 | var el; 224 | var i; 225 | 226 | for (i = 0; i < len; i++) { 227 | 228 | el = data.elements[i]; 229 | 230 | // Prepare the data to pass to the callback 231 | responseData.el = el; 232 | 233 | if (eventType === 'scroll') { 234 | 235 | responseData.direction = getScrolledDirection.call(this, getScrolledAxis.call(this)); 236 | 237 | } 238 | 239 | if (isElementInView.call(this, el)) { 240 | 241 | if (!el.classList.contains(inViewClass)) { 242 | 243 | // Add a class hook and fire a callback for every element that just came into view 244 | 245 | el.classList.add(inViewClass); 246 | config.onElementInView.call(this, responseData); 247 | 248 | if (config.watchOnce) { 249 | 250 | // Remove this element so we don't check it again next time 251 | 252 | data.elements.splice(i, 1); 253 | len--; 254 | i--; 255 | 256 | // Flag this element with the ignore class so we don't store it again if a refresh happens 257 | 258 | el.classList.add(config.ignoreClass); 259 | 260 | } 261 | 262 | } 263 | 264 | } else { 265 | 266 | if (el.classList.contains(inViewClass) || eventType === initEvent) { 267 | 268 | // Remove the class hook and fire a callback for every element that just went out of view 269 | 270 | el.classList.remove(inViewClass); 271 | config.onElementOutOfView.call(this, responseData); 272 | 273 | } 274 | 275 | } 276 | 277 | } 278 | 279 | }; 280 | 281 | // Determine if the infinite scroll zone is in view. This could come into view by scrolling or resizing. Initial load must also be accounted for. 282 | var checkInfinite = function(eventType) { 283 | 284 | var data = instanceData[this._id]; 285 | var config = data.config; 286 | var i; 287 | var axis; 288 | var container; 289 | var viewableRange; 290 | var scrollSize; 291 | var callback; 292 | var responseData; 293 | 294 | if (config.infiniteScroll && !data.isInfiniteScrollPaused) { 295 | 296 | axis = ['x', 'y']; 297 | callback = ['onInfiniteXInView', 'onInfiniteYInView']; 298 | container = config.container; 299 | viewableRange = getViewableRange.call(this); 300 | scrollSize = [container.scrollWidth, container.scrollHeight]; 301 | responseData = {}; 302 | 303 | for (i = 0; i < 2; i++) { 304 | 305 | // If a scroll event triggered this check, verify the scroll position actually changed for each axis. This stops horizontal scrolls from triggering infiniteY callbacks and vice versa. In other words, only trigger an infinite callback if that axis was actually scrolled. 306 | 307 | if ((eventType === 'scroll' && hasScrollPositionChanged.call(this, axis[i]) || eventType === 'resize'|| eventType === 'refresh' || eventType === initEvent) && viewableRange[axis[i]].end + config.infiniteOffset >= scrollSize[i]) { 308 | 309 | // We've scrolled/resized all the way to the right/bottom 310 | 311 | responseData.eventType = eventType; 312 | 313 | if (eventType === 'scroll') { 314 | 315 | responseData.direction = getScrolledDirection.call(this, axis[i]); 316 | 317 | } 318 | 319 | config[callback[i]].call(this, responseData); 320 | 321 | } 322 | 323 | } 324 | 325 | } 326 | 327 | }; 328 | 329 | // Add listeners to the scrolling container for each instance 330 | var addListeners = function() { 331 | 332 | var data = instanceData[this._id]; 333 | var scrollingElement = getScrollingElement.call(this); 334 | 335 | scrollingElement.addEventListener('scroll', data.scrollHandler, false); 336 | scrollingElement.addEventListener('resize', data.resizeHandler, false); 337 | 338 | }; 339 | 340 | var removeListeners = function() { 341 | 342 | var data = instanceData[this._id]; 343 | var scrollingElement = getScrollingElement.call(this); 344 | 345 | scrollingElement.removeEventListener('scroll', data.scrollHandler); 346 | scrollingElement.removeEventListener('resize', data.resizeHandler); 347 | 348 | }; 349 | 350 | var getScrollingElement = function() { 351 | 352 | return isContainerWindow.call(this) ? window : instanceData[this._id].config.container; 353 | 354 | }; 355 | 356 | // Get the width and height of viewport/scrolling container 357 | var getViewportSize = function() { 358 | 359 | var size = { 360 | w: instanceData[this._id].config.container.clientWidth, 361 | h: instanceData[this._id].config.container.clientHeight 362 | }; 363 | 364 | return size; 365 | 366 | }; 367 | 368 | // Get the scrollbar position of the scrolling container 369 | var getScrollPosition = function() { 370 | 371 | var pos = {}; 372 | var container; 373 | 374 | if (isContainerWindow.call(this)) { 375 | 376 | pos.left = window.pageXOffset; 377 | pos.top = window.pageYOffset; 378 | 379 | 380 | } else { 381 | 382 | container = instanceData[this._id].config.container; 383 | 384 | pos.left = container.scrollLeft; 385 | pos.top = container.scrollTop; 386 | 387 | } 388 | 389 | return pos; 390 | 391 | }; 392 | 393 | // Get the pixel range currently viewable within the scrolling container 394 | var getViewableRange = function() { 395 | 396 | var range = { 397 | x: {}, 398 | y: {} 399 | }; 400 | var scrollPos = getScrollPosition.call(this); 401 | var viewportSize = getViewportSize.call(this); 402 | 403 | range.x.start = scrollPos.left; 404 | range.x.end = range.x.start + viewportSize.w; 405 | range.x.size = range.x.end - range.x.start; 406 | 407 | range.y.start = scrollPos.top; 408 | range.y.end = range.y.start + viewportSize.h; 409 | range.y.size = range.y.end - range.y.start; 410 | 411 | return range; 412 | 413 | }; 414 | 415 | // Get the pixel range of where this element falls within the scrolling container 416 | var getElementRange = function(el) { 417 | 418 | var range = { 419 | x: {}, 420 | y: {} 421 | }; 422 | var viewableRange = getViewableRange.call(this); 423 | var coords = el.getBoundingClientRect(); 424 | var containerCoords; 425 | 426 | if (isContainerWindow.call(this)) { 427 | 428 | range.x.start = coords.left + viewableRange.x.start; 429 | range.x.end = coords.right + viewableRange.x.start; 430 | 431 | 432 | range.y.start = coords.top + viewableRange.y.start; 433 | range.y.end = coords.bottom + viewableRange.y.start; 434 | 435 | } else { 436 | 437 | containerCoords = instanceData[this._id].config.container.getBoundingClientRect(); 438 | 439 | range.x.start = (coords.left - containerCoords.left) + viewableRange.x.start; 440 | range.x.end = range.x.start + coords.width; 441 | 442 | range.y.start = (coords.top - containerCoords.top) + viewableRange.y.start; 443 | range.y.end = range.y.start + coords.height; 444 | 445 | } 446 | 447 | range.x.size = range.x.end - range.x.start; 448 | range.y.size = range.y.end - range.y.start; 449 | 450 | return range; 451 | 452 | }; 453 | 454 | // Determines which axis was just scrolled (x/horizontal or y/vertical) 455 | var getScrolledAxis = function() { 456 | 457 | if (hasScrollPositionChanged.call(this, 'x')) { 458 | 459 | return 'x'; 460 | 461 | } 462 | 463 | if (hasScrollPositionChanged.call(this, 'y')) { 464 | 465 | return 'y'; 466 | 467 | } 468 | 469 | }; 470 | 471 | var getScrolledDirection = function(axis) { 472 | 473 | var scrollDir = {x: ['right', 'left'], y: ['down', 'up']}; 474 | var position = {x: 'left', y: 'top'}; 475 | var lastScrollPosition = instanceData[this._id].lastScrollPosition; 476 | var curScrollPosition = getScrollPosition.call(this); 477 | 478 | return curScrollPosition[position[axis]] > lastScrollPosition[position[axis]] ? scrollDir[axis][0] : scrollDir[axis][1]; 479 | 480 | }; 481 | 482 | var hasScrollPositionChanged = function(axis) { 483 | 484 | var position = {x: 'left', y: 'top'}; 485 | var lastScrollPosition = instanceData[this._id].lastScrollPosition; 486 | var curScrollPosition = getScrollPosition.call(this); 487 | 488 | return curScrollPosition[position[axis]] !== lastScrollPosition[position[axis]]; 489 | 490 | }; 491 | 492 | var isElementInView = function(el) { 493 | 494 | var viewableRange = getViewableRange.call(this); 495 | var elRange = getElementRange.call(this, el); 496 | 497 | return isElementInVerticalView.call(this, elRange, viewableRange) && isElementInHorizontalView.call(this, elRange, viewableRange); 498 | 499 | }; 500 | 501 | var isElementInVerticalView = function(elRange, viewableRange) { 502 | 503 | var config = instanceData[this._id].config; 504 | 505 | return elRange.y.start < viewableRange.y.end + config.watchOffsetYBottom && elRange.y.end > viewableRange.y.start - config.watchOffsetYTop; 506 | 507 | }; 508 | 509 | var isElementInHorizontalView = function(elRange, viewableRange) { 510 | 511 | var config = instanceData[this._id].config; 512 | 513 | return elRange.x.start < viewableRange.x.end + config.watchOffsetXRight && elRange.x.end > viewableRange.x.start - config.watchOffsetXLeft; 514 | 515 | }; 516 | 517 | var isContainerWindow = function() { 518 | 519 | return instanceData[this._id].config.container === window.document.documentElement; 520 | 521 | }; 522 | 523 | var mergeOptions = function(opts) { 524 | 525 | extend(instanceData[this._id].config, config, opts); 526 | 527 | }; 528 | 529 | var handler = function(e) { 530 | 531 | var eventType = e.type; 532 | 533 | // Protect against the instance being destroyed while we still have queued or pending handler events (via @jsonk000) 534 | if (!instanceData[this._id]) { 535 | 536 | return; 537 | 538 | } 539 | 540 | // For scroll events, only check the viewport if something has changed. Fixes issues when using gestures on a page that doesn't need to scroll. An event would still fire, but the position didn't change because the window/container "bounced" back into place. 541 | if (eventType === 'resize' || hasScrollPositionChanged.call(this, 'x') || hasScrollPositionChanged.call(this, 'y')) { 542 | 543 | checkViewport.call(this, eventType); 544 | 545 | } 546 | 547 | }; 548 | 549 | var ScrollWatch = function(opts) { 550 | 551 | var data; 552 | 553 | // Protect against missing new keyword 554 | if (this instanceof ScrollWatch) { 555 | 556 | Object.defineProperty(this, '_id', {value: instanceId++}); 557 | 558 | // Keep all instance data private, except for the '_id', which will be the key to get the private data for a specific instance 559 | 560 | data = instanceData[this._id] = { 561 | 562 | config: {}, 563 | // The elements to watch for this instance 564 | elements: [], 565 | lastScrollPosition: {top: 0, left: 0}, 566 | isInfiniteScrollPaused: false 567 | 568 | }; 569 | 570 | mergeOptions.call(this, opts); 571 | 572 | // In order to remove listeners later and keep a correct reference to 'this', give each instance it's own event handler 573 | if (data.config.debounce) { 574 | 575 | data.scrollHandler = debounce(handler.bind(this), data.config.scrollDebounce, data.config.debounceTriggerLeading); 576 | data.resizeHandler = debounce(handler.bind(this), data.config.resizeDebounce, data.config.debounceTriggerLeading); 577 | 578 | } else { 579 | 580 | data.scrollHandler = throttle(handler.bind(this), data.config.scrollThrottle, this); 581 | data.resizeHandler = throttle(handler.bind(this), data.config.resizeThrottle, this); 582 | 583 | } 584 | 585 | saveContainerElement.call(this); 586 | addListeners.call(this); 587 | saveElements.call(this); 588 | checkViewport.call(this, initEvent); 589 | 590 | } else { 591 | 592 | return new ScrollWatch(opts); 593 | 594 | } 595 | 596 | }; 597 | 598 | ScrollWatch.prototype = { 599 | 600 | // Should be manually called by user after loading in new content 601 | refresh: function() { 602 | 603 | saveElements.call(this); 604 | checkViewport.call(this, 'refresh'); 605 | 606 | }, 607 | 608 | destroy: function() { 609 | 610 | removeListeners.call(this); 611 | delete instanceData[this._id]; 612 | 613 | }, 614 | 615 | updateWatchOffsetXLeft: function(offset) { 616 | 617 | instanceData[this._id].config.watchOffsetXLeft = offset; 618 | 619 | }, 620 | 621 | updateWatchOffsetXRight: function(offset) { 622 | 623 | instanceData[this._id].config.watchOffsetXRight = offset; 624 | 625 | }, 626 | 627 | updateWatchOffsetYTop: function(offset) { 628 | 629 | instanceData[this._id].config.watchOffsetYTop = offset; 630 | 631 | }, 632 | 633 | updateWatchOffsetYBottom: function(offset) { 634 | 635 | instanceData[this._id].config.watchOffsetYBottom = offset; 636 | 637 | }, 638 | 639 | pauseInfiniteScroll: function() { 640 | 641 | instanceData[this._id].isInfiniteScrollPaused = true; 642 | 643 | }, 644 | 645 | resumeInfiniteScroll: function() { 646 | 647 | instanceData[this._id].isInfiniteScrollPaused = false; 648 | 649 | } 650 | 651 | }; 652 | --------------------------------------------------------------------------------