├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── id_rsa_nginfinite.enc ├── inert-bower.json ├── make-bower-build.sh ├── package.json ├── src └── infinite-scroll.js └── test ├── .eslintrc.json ├── protractor.conf.js └── spec └── ng-infinite-scroll.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["modern-node/6.3", "es2016", "stage-1"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb-base", 4 | "rules": { 5 | "arrow-parens": [2, "as-needed"], 6 | "curly": [2, "all"], 7 | "import/no-unresolved": 0 8 | }, 9 | "env": { 10 | "browser": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | 42 | compile 43 | build 44 | .tmp 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 6 4 | before_install: 5 | - "sh -e /etc/init.d/xvfb start" 6 | env: 7 | global: 8 | - ENCRYPTION_LABEL: 61bce606080b 9 | - COMMIT_AUTHOR_EMAIL: 'ng-infinite-scroll-bower@graingert.co.uk' 10 | before_script: 11 | - "export DISPLAY=:99.0" 12 | - until xwininfo -root > /dev/null 2>&1; do echo "waiting for xvfb"; done 13 | cache: 14 | directories: 15 | - node_modules 16 | after_success: ./make-bower-build.sh 17 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | loadGruntTasks = require 'load-grunt-tasks' 2 | module.exports = (grunt) -> 3 | loadGruntTasks(grunt) 4 | 5 | grunt.initConfig 6 | pkg: grunt.file.readJSON 'package.json' 7 | meta: 8 | banner: '/* <%= pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %> */\n' 9 | eslint: 10 | target: ['src', 'test'] 11 | clean: 12 | options: 13 | force: true 14 | build: ["compile/**", "build/**"] 15 | babel: 16 | compile: 17 | files: [ 18 | { 19 | expand: true 20 | cwd: 'src/' 21 | src: '**/*.js' 22 | dest: 'compile/' 23 | ext: '.js' 24 | } 25 | ] 26 | options: 27 | presets: ["es2015", "es2016", "stage-1"] 28 | plugins: ["add-module-exports", "transform-es2015-modules-umd"] 29 | concat: 30 | options: 31 | banner: '<%= meta.banner %>' 32 | dist: 33 | src: 'compile/**/*.js' 34 | dest: 'build/ng-infinite-scroll.js' 35 | uglify: 36 | options: 37 | banner: '<%= meta.banner %>' 38 | dist: 39 | src: ['build/ng-infinite-scroll.js'] 40 | dest: 'build/ng-infinite-scroll.min.js' 41 | connect: 42 | testserver: 43 | options: 44 | port: 8000 45 | hostname: '0.0.0.0' 46 | middleware: (connect, options) -> 47 | base = if Array.isArray(options.base) then options.base[options.base.length - 1] else options.base 48 | [connect.static(base)] 49 | protractor: 50 | local: 51 | options: 52 | configFile: 'test/protractor.conf.js' 53 | args: 54 | params: 55 | testThrottleValue: 500 56 | 57 | grunt.registerTask 'webdriver', () -> 58 | done = this.async() 59 | p = require('child_process').spawn('node', ['node_modules/protractor/bin/webdriver-manager', 'update']) 60 | p.stdout.pipe(process.stdout) 61 | p.stderr.pipe(process.stderr) 62 | p.on 'exit', (code) -> 63 | if code isnt 0 then grunt.fail.warn('Webdriver failed to update') 64 | done() 65 | 66 | grunt.registerTask 'sauce-connect', () -> 67 | done = this.async() 68 | require('sauce-connect-launcher')({username: sauceUser, accessKey: sauceKey}, (err, sauceConnectProcess) -> 69 | if err then console.error(err.message) 70 | else done() 71 | ) 72 | 73 | grunt.registerTask 'default', ['eslint', 'clean', 'babel', 'concat', 'uglify'] 74 | grunt.registerTask 'test:protractor-local', [ 75 | 'default', 76 | 'webdriver', 77 | 'connect:testserver', 78 | 'protractor:local' 79 | ] 80 | 81 | grunt.registerTask 'test:protractor-travis', [ 82 | 'connect:testserver', 83 | 'sauce-connect', 84 | 'protractor:travis' 85 | ] 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Michelle Tilley 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [**Alternative project**: Angular Material's md-virtual-repeat](https://material.angularjs.org/latest/api/directive/mdVirtualRepeat) 2 | 3 | [**Maintainer help needed**: I'm looking for fellows that are willing to help me maintain and improve this project.](https://github.com/sroze/ngInfiniteScroll/issues/267) 4 | 5 | --- 6 | 7 |  8 | 9 | [](https://travis-ci.org/sroze/ngInfiniteScroll) 10 | 11 | ngInfiniteScroll is a directive for [AngularJS](http://angularjs.org/) to evaluate an expression when the bottom of the directive's element approaches the bottom of the browser window, which can be used to implement infinite scrolling. 12 | 13 | Demos 14 | ----- 15 | 16 | Check out the running demos [at the ngInfiniteScroll web site](http://sroze.github.com/ngInfiniteScroll/demos.html). 17 | 18 | Version Numbers 19 | --------------- 20 | 21 | ngInfinite Scroll follows [semantic versioning](http://semver.org/). 22 | 23 | Getting Started 24 | --------------- 25 | 26 | * Install it with: 27 | * [npm](https://www.npmjs.com) via `npm install --save ng-infinite-scroll` 28 | * Import ng-infinite-scroll and angular. 29 | 30 | ```js 31 | import angular from 'angular'; 32 | import ngInfiniteScroll from 'ng-infinite-scroll'; 33 | ``` 34 | 35 | * Ensure that your application module specifies ngInfiniteScroll as a dependency: 36 | 37 | ```js 38 | const MODULE_NAME = 'myApplication'; 39 | angular.module(MODULE_NAME, [ngInfiniteScroll]); 40 | export default MODULE_NAME; 41 | ``` 42 | 43 | * Use the directive by specifying an `infinite-scroll` attribute on an element. 44 | 45 | ```html 46 |
47 | ``` 48 | 49 | Note that the directive does not use the `ng` prefix, as that prefix is reserved for the core Angular module. 50 | 51 | Detailed Documentation 52 | ---------------------- 53 | 54 | ngInfiniteScroll accepts several attributes to customize the behavior of the directive; detailed instructions can be found [on the ngInfiniteScroll web site](http://sroze.github.com/ngInfiniteScroll/documentation.html). 55 | 56 | Ports 57 | ----- 58 | 59 | If you use [AngularDart](https://github.com/angular/angular.dart), Juha Komulainen has [a port of the project](http://pub.dartlang.org/packages/ng_infinite_scroll) you can use. 60 | 61 | License 62 | ------- 63 | 64 | ngInfiniteScroll is licensed under the MIT license. See the LICENSE file for more details. 65 | 66 | Testing 67 | ------- 68 | 69 | ngInfiniteScroll uses Protractor for testing. 70 | Note that you will need to have Chrome browser. 71 | 72 | npm install 73 | npm run test 74 | 75 | Thank you very much @pomerantsev for your work on these Protractor tests. 76 | 77 | Bower 78 | ----- 79 | 80 | While a Bower repo has been created for legacy use, it is still recommened to 81 | use npm and a module bundler (webpack, rollup, SystemJS) to use 82 | `ng-infinite-scroll`. 83 | 84 | To install using bower: 85 | 86 | ``` 87 | bower install ngInfiniteScroll 88 | ``` 89 | -------------------------------------------------------------------------------- /id_rsa_nginfinite.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sroze/ngInfiniteScroll/5035fd2d563eb09447bc500e5b754b21fbd3b92c/id_rsa_nginfinite.enc -------------------------------------------------------------------------------- /inert-bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngInfiniteScroll", 3 | "main": "build/ng-infinite-scroll.js", 4 | "license": "MIT", 5 | "dependencies": { 6 | "angular": ">=1.2.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /make-bower-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e # Exit with nonzero exit code if anything fails 3 | 4 | SOURCE_BRANCH="master" 5 | TARGET_BRANCH="master" 6 | 7 | # Run only test cases if this is not a release 8 | if [ -z "$TRAVIS_TAG" ]; then 9 | exit 0 10 | fi 11 | 12 | # Get the deploy key by using Travis's stored variables to decrypt deploy_key.enc 13 | ENCRYPTED_KEY_VAR="encrypted_${ENCRYPTION_LABEL}_key" 14 | ENCRYPTED_IV_VAR="encrypted_${ENCRYPTION_LABEL}_iv" 15 | ENCRYPTED_KEY=${!ENCRYPTED_KEY_VAR} 16 | ENCRYPTED_IV=${!ENCRYPTED_IV_VAR} 17 | openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in id_rsa_nginfinite.enc -out deploy_key -d 18 | chmod 600 deploy_key 19 | eval `ssh-agent -s` 20 | ssh-add deploy_key 21 | 22 | BOWER_REPO='git@github.com:ng-infinite-scroll/ng-infinite-scroll-bower.git' 23 | 24 | BOWER_REPO_DIR='out' 25 | CWD="$PWD" 26 | 27 | git clone $BOWER_REPO $BOWER_REPO_DIR 28 | cd $BOWER_REPO_DIR 29 | git checkout $TARGET_BRANCH || git checkout -b $TARGET_BRANCH 30 | 31 | git config user.name "Travis CI" 32 | git config user.email "$COMMIT_AUTHOR_EMAIL" 33 | 34 | # Copy all build content to the bower repo 35 | cp -r ../build . 36 | cp ../inert-bower.json bower.json 37 | cp ../LICENSE LICENSE 38 | 39 | git add build 40 | git add bower.json 41 | 42 | git commit -am "Release version $TRAVIS_TAG" 43 | git tag -d "$TRAVIS_TAG" || true 44 | git tag "$TRAVIS_TAG" 45 | git push origin -f $TARGET_BRANCH $TRAVIS_TAG 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-infinite-scroll", 3 | "version": "1.3.0", 4 | "description": "Infinite scrolling for AngularJS", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/sroze/ngInfiniteScroll.git" 8 | }, 9 | "main": "build/ng-infinite-scroll.js", 10 | "browser": "build/ng-infinite-scroll.js", 11 | "scripts": { 12 | "prepublish": "grunt", 13 | "test": "grunt test:protractor-local" 14 | }, 15 | "author": { 16 | "name": "Michelle Tilley", 17 | "email": "michelle@michelletilley.net", 18 | "url": "http://michelletilley.net" 19 | }, 20 | "contributors": [ 21 | { 22 | "name": "Samuel ROZE", 23 | "email": "samuel.roze@gmail.com", 24 | "url": "http://sroze.io" 25 | } 26 | ], 27 | "license": "MIT", 28 | "readmeFilename": "README.md", 29 | "devDependencies": { 30 | "angular": "^1.5.8", 31 | "babel-eslint": "^6.1.2", 32 | "babel-plugin-add-module-exports": "^0.2.1", 33 | "babel-plugin-transform-es2015-modules-umd": "^6.12.0", 34 | "babel-preset-es2015": "^6.13.2", 35 | "babel-preset-es2016": "^6.11.3", 36 | "babel-preset-modern-node": "^2.3.0", 37 | "babel-preset-stage-1": "^6.13.0", 38 | "babel-register": "^6.11.6", 39 | "coffee-script": "~1.8.0", 40 | "eslint": "^3.2.2", 41 | "eslint-config-airbnb-base": "^5.0.1", 42 | "eslint-plugin-import": "^1.12.0", 43 | "grunt": "~0.4.5", 44 | "grunt-babel": "^6.0.0", 45 | "grunt-cli": "^1.2.0", 46 | "grunt-contrib-clean": "~0.6.0", 47 | "grunt-contrib-concat": "~0.5.0", 48 | "grunt-contrib-connect": "0.8.0", 49 | "grunt-contrib-uglify": "~0.6.0", 50 | "grunt-eslint": "^19.0.0", 51 | "grunt-protractor-runner": "1.1.4", 52 | "load-grunt-tasks": "^3.5.0", 53 | "mkdirp": "0.5.0", 54 | "protractor": "1.4.0", 55 | "sauce-connect-launcher": "0.9.0" 56 | }, 57 | "peerDependencies": { 58 | "angular": "^1.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/infinite-scroll.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | 3 | const MODULE_NAME = 'infinite-scroll'; 4 | 5 | angular.module(MODULE_NAME, []) 6 | .value('THROTTLE_MILLISECONDS', null) 7 | .directive('infiniteScroll', [ 8 | '$rootScope', '$window', '$interval', 'THROTTLE_MILLISECONDS', 9 | ($rootScope, $window, $interval, THROTTLE_MILLISECONDS) => 10 | ({ 11 | scope: { 12 | infiniteScroll: '&', 13 | infiniteScrollContainer: '=', 14 | infiniteScrollDistance: '=', 15 | infiniteScrollDisabled: '=', 16 | infiniteScrollUseDocumentBottom: '=', 17 | infiniteScrollListenForEvent: '@', 18 | }, 19 | 20 | link(scope, elem, attrs) { 21 | const windowElement = angular.element($window); 22 | 23 | let scrollDistance = null; 24 | let scrollEnabled = null; 25 | let checkWhenEnabled = null; 26 | let container = null; 27 | let immediateCheck = true; 28 | let useDocumentBottom = false; 29 | let unregisterEventListener = null; 30 | let checkInterval = false; 31 | 32 | function height(element) { 33 | const el = element[0] || element; 34 | 35 | if (isNaN(el.offsetHeight)) { 36 | return el.document.documentElement.clientHeight; 37 | } 38 | return el.offsetHeight; 39 | } 40 | 41 | function pageYOffset(element) { 42 | const el = element[0] || element; 43 | 44 | if (isNaN(window.pageYOffset)) { 45 | return el.document.documentElement.scrollTop; 46 | } 47 | return el.ownerDocument.defaultView.pageYOffset; 48 | } 49 | 50 | function offsetTop(element) { 51 | if (!(!element[0].getBoundingClientRect || element.css('none'))) { 52 | return element[0].getBoundingClientRect().top + pageYOffset(element); 53 | } 54 | return undefined; 55 | } 56 | 57 | // infinite-scroll specifies a function to call when the window, 58 | // or some other container specified by infinite-scroll-container, 59 | // is scrolled within a certain range from the bottom of the 60 | // document. It is recommended to use infinite-scroll-disabled 61 | // with a boolean that is set to true when the function is 62 | // called in order to throttle the function call. 63 | function defaultHandler() { 64 | let containerBottom; 65 | let elementBottom; 66 | if (container === windowElement) { 67 | containerBottom = height(container) + pageYOffset(container[0].document.documentElement); 68 | elementBottom = offsetTop(elem) + height(elem); 69 | } else { 70 | containerBottom = height(container); 71 | let containerTopOffset = 0; 72 | if (offsetTop(container) !== undefined) { 73 | containerTopOffset = offsetTop(container); 74 | } 75 | elementBottom = (offsetTop(elem) - containerTopOffset) + height(elem); 76 | } 77 | 78 | if (useDocumentBottom) { 79 | elementBottom = height((elem[0].ownerDocument || elem[0].document).documentElement); 80 | } 81 | 82 | const remaining = elementBottom - containerBottom; 83 | const shouldScroll = remaining <= (height(container) * scrollDistance) + 1; 84 | 85 | if (shouldScroll) { 86 | checkWhenEnabled = true; 87 | 88 | if (scrollEnabled) { 89 | if (scope.$$phase || $rootScope.$$phase) { 90 | scope.infiniteScroll(); 91 | } else { 92 | scope.$apply(scope.infiniteScroll); 93 | } 94 | } 95 | } else { 96 | if (checkInterval) { $interval.cancel(checkInterval); } 97 | checkWhenEnabled = false; 98 | } 99 | } 100 | 101 | // The optional THROTTLE_MILLISECONDS configuration value specifies 102 | // a minimum time that should elapse between each call to the 103 | // handler. N.b. the first call the handler will be run 104 | // immediately, and the final call will always result in the 105 | // handler being called after the `wait` period elapses. 106 | // A slimmed down version of underscore's implementation. 107 | function throttle(func, wait) { 108 | let timeout = null; 109 | let previous = 0; 110 | 111 | function later() { 112 | previous = new Date().getTime(); 113 | $interval.cancel(timeout); 114 | timeout = null; 115 | return func.call(); 116 | } 117 | 118 | function throttled() { 119 | const now = new Date().getTime(); 120 | const remaining = wait - (now - previous); 121 | if (remaining <= 0) { 122 | $interval.cancel(timeout); 123 | timeout = null; 124 | previous = now; 125 | func.call(); 126 | } else if (!timeout) { 127 | timeout = $interval(later, remaining, 1); 128 | } 129 | } 130 | 131 | return throttled; 132 | } 133 | 134 | const handler = (THROTTLE_MILLISECONDS != null) ? 135 | throttle(defaultHandler, THROTTLE_MILLISECONDS) : 136 | defaultHandler; 137 | 138 | function handleDestroy() { 139 | container.unbind('scroll', handler); 140 | if (unregisterEventListener != null) { 141 | unregisterEventListener(); 142 | unregisterEventListener = null; 143 | } 144 | if (checkInterval) { 145 | $interval.cancel(checkInterval); 146 | } 147 | } 148 | 149 | scope.$on('$destroy', handleDestroy); 150 | 151 | // infinite-scroll-distance specifies how close to the bottom of the page 152 | // the window is allowed to be before we trigger a new scroll. The value 153 | // provided is multiplied by the container height; for example, to load 154 | // more when the bottom of the page is less than 3 container heights away, 155 | // specify a value of 3. Defaults to 0. 156 | function handleInfiniteScrollDistance(v) { 157 | scrollDistance = parseFloat(v) || 0; 158 | } 159 | 160 | scope.$watch('infiniteScrollDistance', handleInfiniteScrollDistance); 161 | // If I don't explicitly call the handler here, tests fail. Don't know why yet. 162 | handleInfiniteScrollDistance(scope.infiniteScrollDistance); 163 | 164 | // infinite-scroll-disabled specifies a boolean that will keep the 165 | // infnite scroll function from being called; this is useful for 166 | // debouncing or throttling the function call. If an infinite 167 | // scroll is triggered but this value evaluates to true, then 168 | // once it switches back to false the infinite scroll function 169 | // will be triggered again. 170 | function handleInfiniteScrollDisabled(v) { 171 | scrollEnabled = !v; 172 | if (scrollEnabled && checkWhenEnabled) { 173 | checkWhenEnabled = false; 174 | handler(); 175 | } 176 | } 177 | 178 | scope.$watch('infiniteScrollDisabled', handleInfiniteScrollDisabled); 179 | // If I don't explicitly call the handler here, tests fail. Don't know why yet. 180 | handleInfiniteScrollDisabled(scope.infiniteScrollDisabled); 181 | 182 | // use the bottom of the document instead of the element's bottom. 183 | // This useful when the element does not have a height due to its 184 | // children being absolute positioned. 185 | function handleInfiniteScrollUseDocumentBottom(v) { 186 | useDocumentBottom = v; 187 | } 188 | 189 | scope.$watch('infiniteScrollUseDocumentBottom', handleInfiniteScrollUseDocumentBottom); 190 | handleInfiniteScrollUseDocumentBottom(scope.infiniteScrollUseDocumentBottom); 191 | 192 | // infinite-scroll-container sets the container which we want to be 193 | // infinte scrolled, instead of the whole window. Must be an 194 | // Angular or jQuery element, or, if jQuery is loaded, 195 | // a jQuery selector as a string. 196 | function changeContainer(newContainer) { 197 | if (container != null) { 198 | container.unbind('scroll', handler); 199 | } 200 | 201 | container = newContainer; 202 | if (newContainer != null) { 203 | container.bind('scroll', handler); 204 | } 205 | } 206 | 207 | changeContainer(windowElement); 208 | 209 | if (scope.infiniteScrollListenForEvent) { 210 | unregisterEventListener = $rootScope.$on(scope.infiniteScrollListenForEvent, handler); 211 | } 212 | 213 | function handleInfiniteScrollContainer(newContainer) { 214 | // TODO: For some reason newContainer is sometimes null instead 215 | // of the empty array, which Angular is supposed to pass when the 216 | // element is not defined 217 | // (https://github.com/sroze/ngInfiniteScroll/pull/7#commitcomment-5748431). 218 | // So I leave both checks. 219 | if ((!(newContainer != null)) || newContainer.length === 0) { 220 | return; 221 | } 222 | 223 | let newerContainer; 224 | 225 | if (newContainer.nodeType && newContainer.nodeType === 1) { 226 | newerContainer = angular.element(newContainer); 227 | } else if (typeof newContainer.append === 'function') { 228 | newerContainer = angular.element(newContainer[newContainer.length - 1]); 229 | } else if (typeof newContainer === 'string') { 230 | newerContainer = angular.element(document.querySelector(newContainer)); 231 | } else { 232 | newerContainer = newContainer; 233 | } 234 | 235 | if (newerContainer == null) { 236 | throw new Error('invalid infinite-scroll-container attribute.'); 237 | } 238 | changeContainer(newerContainer); 239 | } 240 | 241 | scope.$watch('infiniteScrollContainer', handleInfiniteScrollContainer); 242 | handleInfiniteScrollContainer(scope.infiniteScrollContainer || []); 243 | 244 | // infinite-scroll-parent establishes this element's parent as the 245 | // container infinitely scrolled instead of the whole window. 246 | if (attrs.infiniteScrollParent != null) { 247 | changeContainer(angular.element(elem.parent())); 248 | } 249 | 250 | // infinte-scoll-immediate-check sets whether or not run the 251 | // expression passed on infinite-scroll for the first time when the 252 | // directive first loads, before any actual scroll. 253 | if (attrs.infiniteScrollImmediateCheck != null) { 254 | immediateCheck = scope.$eval(attrs.infiniteScrollImmediateCheck); 255 | } 256 | 257 | function intervalCheck() { 258 | if (immediateCheck) { 259 | handler(); 260 | } 261 | return $interval.cancel(checkInterval); 262 | } 263 | 264 | checkInterval = $interval(intervalCheck); 265 | return checkInterval; 266 | }, 267 | }), 268 | 269 | ]); 270 | 271 | export default MODULE_NAME; 272 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "protractor": true, 4 | "mocha": true 5 | }, 6 | "globals": { 7 | "expect": false 8 | }, 9 | "rules": { 10 | "import/no-extraneous-dependencies": 0, 11 | "prefer-arrow-callback": 0, 12 | "func-names": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/protractor.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | 3 | exports.config = { 4 | specs: ['**/*.spec.js'], 5 | baseUrl: 'http://localhost:8000/', 6 | allScriptsTimeout: 30000, 7 | getPageTimeout: 30000, 8 | multiCapabilities: [ 9 | { 10 | browserName: 'chrome', 11 | chromeOptions: { args: ['--no-sandbox'] }, 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /test/spec/ng-infinite-scroll.spec.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import mkdirp from 'mkdirp'; 3 | 4 | function retry(times, fn) { 5 | function doRetry() { 6 | for (let i = 0; i < (times - 1); i += 1) { 7 | try { 8 | return fn(); 9 | } catch (e) { 10 | browser.restart(); 11 | } 12 | } 13 | 14 | return fn(); 15 | } 16 | 17 | return doRetry; 18 | } 19 | 20 | const containers = { 21 | window: { 22 | start: '', 23 | end: '', 24 | attr: '', 25 | }, 26 | parent: { 27 | start: "78 | {{$index}} 79 |
80 |