├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── angular-scroll.js ├── angular-scroll.min.js ├── angular-scroll.min.js.map ├── bower.json ├── example ├── container.html ├── index.html └── toc.html ├── gulpfile.js ├── index.js ├── package.json ├── src ├── directives │ ├── scroll-container.js │ ├── scrollspy.js │ ├── smooth-scroll.js │ └── spy-context.js ├── helpers.js ├── module.js └── services │ ├── request-animation.js │ ├── scroll-container-api.js │ └── spy-api.js └── test ├── e2e ├── pages │ ├── container-page.js │ └── default-page.js └── scenarios.js ├── karma.conf.js ├── protractor.conf.js └── unit ├── defaultsSpec.js ├── helpersSpec.js └── servicesSpec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | .idea 15 | node_modules 16 | bower_components 17 | npm-debug.log 18 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": ["angular"], 3 | "browser": true, 4 | "strict": true 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | sudo: false 4 | 5 | node_js: 6 | - "0.12" 7 | 8 | before_install: 9 | - npm install -g bower 10 | - bower install 11 | - export CHROME_BIN=chromium-browser 12 | - export DISPLAY=:99.0 13 | - sh -e /etc/init.d/xvfb start 14 | 15 | script: npm run test-ci 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Issues 2 | ------ 3 | 4 | **Please provide clear steps to reproduce your issue**. This is preferably done in a [plunker](http://plnkr.co/), a video or just a link to your project. 5 | 6 | Also nice to include info about your browser environment, version of angular/angular-scroll you are using and other libraries (such as jQuery etc). 7 | 8 | 9 | Pull Requests 10 | ------------- 11 | 12 | * Don't include the compiled assets (`angular-scroll.*`) in your PR. 13 | * Only edit the source files (ie not the `angular-scroll.*` files). 14 | * Tests are welcome and don't forget to run existing via `npm run test-single-run` and `npm run protractor` before comitting. 15 | * Lint your code by running `gulp lint` before comitting. 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Durated 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-scroll 2 | ============== 3 | 4 | Angular is only dependency (no jQuery). 8K minified or 2K gzipped. 5 | 6 | Example 7 | ------- 8 | Check out [the live demo](http://oblador.github.io/angular-scroll/) or the [source code](https://github.com/oblador/angular-scroll/blob/master/example/index.html). 9 | 10 | Install 11 | ------- 12 | 13 | #### With bower: 14 | 15 | $ bower install angular-scroll 16 | 17 | #### With npm (for use with browserify): 18 | 19 | $ npm install angular-scroll 20 | 21 | You can also download the [production version](https://raw.github.com/oblador/angular-scroll/master/angular-scroll.min.js) or the [development version](https://raw.github.com/oblador/angular-scroll/master/angular-scroll.js). 22 | 23 | If you prefer a CDN hosted version (which might speed up your load times), check out [cdnjs.com/libraries/angular-scroll](https://cdnjs.com/libraries/angular-scroll). 24 | 25 | 26 | Don't forget to add `duScroll` to your module dependencies. 27 | 28 | `angular.element` Scroll API 29 | ---------------------------- 30 | 31 | This module extends the `angular.element` object with a few jQuery like functions. Note that `$document` is an `angular.element`, for usage example see below. In case of name collisions existing jQuery or jqlite functions will be preserved, just use the prefixed version: ie `.duScrollTo()` instead of `.scrollTo()`. 32 | 33 | #### `.scrollTo( left, top [, duration [, easing ] ] )` 34 | Scrolls the element/window to the specified left/top position. If `duration` is specified the scrolling is animated for n milliseconds. If `easing` is ommited the animation will default to the `duScrollEasing` function. 35 | 36 | #### `.scrollTo( element [, offset, [, duration [, easing ] ] ] )` 37 | Alias of `.scrollToElement`. 38 | 39 | #### `.scrollToElement( element [, offset, [, duration [, easing ] ] ] )` 40 | Scrolls to the specified element, if `offset` is passed it will be subtracted from the elements position which is good if one uses floating menus. 41 | 42 | #### `.scrollToElementAnimated( element [, offset, [, duration [, easing ] ] ] )` 43 | Convenience function. Works exactly the same as `scrollToElement` but uses the default values from `duScrollOffset`, `duScrollDuration` and `duScrollEasing` unless otherwise specified. 44 | 45 | #### `.scrollTop|scrollLeft( )` 46 | Returns current scroll position. 47 | 48 | #### `.scrollTop|scrollLeft( top [, duration [, easing ] ] )` 49 | Scrolls to specified position in either axis, with optional animation. 50 | 51 | #### `.scrollTopAnimated|scrollLeftAnimated( top [, duration [, easing ] ] )` 52 | Convenience function like `scrollToElementAnimated` but for `scrollTop`/`scrollLeft`. 53 | 54 | #### Promises 55 | Animated scrolling returns a `$q` promise, it will resolve when the scrolling has finished or be rejected if cancelled (by starting another scroll animation before it finished). 56 | 57 | #### Example 58 | ```js 59 | angular.module('myApp', ['duScroll']). 60 | controller('myCtrl', function($scope, $document) { 61 | var top = 400; 62 | var duration = 2000; //milliseconds 63 | 64 | //Scroll to the exact position 65 | $document.scrollTop(top, duration).then(function() { 66 | console && console.log('You just scrolled to the top!'); 67 | }); 68 | 69 | var offset = 30; //pixels; adjust for floating menu, context etc 70 | //Scroll to #some-id with 30 px "padding" 71 | //Note: Use this in a directive, not with document.getElementById 72 | var someElement = angular.element(document.getElementById('some-id')); 73 | $document.scrollToElement(someElement, offset, duration); 74 | } 75 | ); 76 | ``` 77 | 78 | The above example can be achieved by configuration instead of arguments: 79 | 80 | ```js 81 | angular.module('myApp', ['duScroll']) 82 | .value('duScrollDuration', 2000) 83 | .value('duScrollOffset', 30) 84 | .controller('myCtrl', function($scope, $document) { 85 | $document.scrollTopAnimated(400).then(function() { 86 | console && console.log('You just scrolled to the top!'); 87 | }); 88 | 89 | var someElement = angular.element(document.getElementById('some-id')); 90 | $document.scrollToElementAnimated(someElement); 91 | } 92 | ); 93 | ``` 94 | 95 | 96 | Directives 97 | ---------- 98 | 99 | ### `du-smooth-scroll` 100 | Provides smooth anchor scrolling. 101 | ```html 102 | Scroll it! 103 | ``` 104 | 105 | If you, for some reason, do not want to use the `href` attribute as fallback, just use the `du-smooth-scroll` attribute instead but without leading #. Example: ``. 106 | 107 | ### `du-scrollspy` 108 | Observes whether the target element is at the top of the viewport (or container) and adds an `active` class if so. Takes optional `offset` and `duration` attributes which is passed on to `.scrollTo`, 109 | 110 | ```html 111 | Am i active? 112 | ``` 113 | 114 | or together with Bootstrap 115 | 116 | ```html 117 | 120 | ``` 121 | 122 | ### `du-spy-context` 123 | Enables multiple sets of spies on the same target element. Takes optional `offset` attribute to 124 | 125 | ```html 126 | 129 | 132 | ``` 133 | ### `du-scroll-container` 134 | Modifies behavior of `du-scrollspy` and `du-smooth-scroll` to observe/scroll within and element instead of the window/document. Good for modals/elements with `overflow: auto/scroll`. 135 | 136 | ```html 137 |
138 |

This is the top

139 |

Scroll to me, or the top

140 |
141 | ``` 142 | 143 | If your links lie outside of the scrollable element, wrap them with the `du-scroll-container` directive and send the element id as argument: 144 | 145 | ```html 146 | 149 |
150 | [...] 151 |
152 | ``` 153 | 154 | ### [All in together now](http://www.youtube.com/watch?v=cx4KtTezEFg&feature=kp) 155 | The directives play well together, try [the demo](http://oblador.github.io/angular-scroll/container.html) or inspect its [source code](https://github.com/oblador/angular-scroll/blob/master/example/container.html). 156 | 157 | ```html 158 | 161 | 164 |
165 | [...] 166 |
167 | ``` 168 | 169 | Observing Scroll Position 170 | ------------------------- 171 | 172 | **NOTE:** the `$duScrollChanged` event and the `scrollPosition` service are deprecated. Use `angular.element().on()` together with `.scrollTop()` instead. 173 | 174 | ```js 175 | angular.module('myApp', ['duScroll']). 176 | controller('MyCtrl', function($scope, $document){ 177 | $document.on('scroll', function() { 178 | console.log('Document scrolled to ', $document.scrollLeft(), $document.scrollTop()); 179 | }); 180 | var container = angular.element(document.getElementById('container')); 181 | container.on('scroll', function() { 182 | console.log('Container scrolled to ', container.scrollLeft(), container.scrollTop()); 183 | }); 184 | } 185 | ); 186 | ``` 187 | 188 | Configuration 189 | ------------- 190 | 191 | ### Scroll speed 192 | Duration is defined in milliseconds. 193 | 194 | To set a scroll duration on a single anchor: 195 | ```html 196 | Scroll it! 197 | ``` 198 | 199 | To change the default duration: 200 | ```js 201 | angular.module('myApp', ['duScroll']).value('duScrollDuration', 5000); 202 | ``` 203 | 204 | ### Scroll easing 205 | Set the `duScrollEasing` value to a function that takes and returns a value between 0 to 1. Here's [a few examples](https://gist.github.com/gre/1650294) to choose from. 206 | 207 | ```js 208 | function invertedEasingFunction(x) { 209 | return 1-x; 210 | } 211 | angular.module('myApp', ['duScroll']).value('duScrollEasing', invertedEasingFunction); 212 | ``` 213 | 214 | You can also pass a custom easing function as the fourth argument in `scrollTo`. 215 | 216 | ### Debounce Scroll Events 217 | Set the `duScrollSpyWait` value in milliseconds to debounce the handler and prevent it from triggering frequent events and increase performance for large pages and/or navigations with expanding nodes. 218 | 219 | ```js 220 | angular.module('myApp', ['duScroll']).value('duScrollSpyWait', 1000); 221 | ``` 222 | 223 | ### Greedy option 224 | Set the `duScrollGreedy` value to `true` if the elements you are observing are not wrapping the whole section you want to observe, but merely the first one in the section (such as headlines). 225 | 226 | ```js 227 | angular.module('myApp', ['duScroll']).value('duScrollGreedy', true); 228 | ``` 229 | 230 | ### Offset 231 | To change default offset (in pixels) for the `du-smooth-scroll` directive: 232 | 233 | ```js 234 | angular.module('myApp', ['duScroll']).value('duScrollOffset', 30); 235 | ``` 236 | 237 | ### When to cancel scroll animation 238 | Specify on which events on the container the scroll animation should be cancelled by modifying `duScrollCancelOnEvents`, set to `false` to disable entirely as shown below. Defaults to `scroll mousedown mousewheel touchmove keydown`. 239 | 240 | ```js 241 | angular.module('myApp', ['duScroll']).value('duScrollCancelOnEvents', false); 242 | ``` 243 | 244 | ### Bottom spy 245 | To make the last `du-scrollspy` link active when scroll reaches page/container bottom: 246 | 247 | ```js 248 | angular.module('myApp', ['duScroll']).value('duScrollBottomSpy', true); 249 | ``` 250 | 251 | ### Active class 252 | Specify the active class name to apply to a link when it is active, default is `active`. 253 | 254 | ```js 255 | angular.module('myApp', ['duScroll']).value('duScrollActiveClass', 'custom-class'); 256 | ``` 257 | 258 | Events 259 | ------ 260 | 261 | The `duScrollspy` directive fires the global events `duScrollspy:becameActive` and `duScrollspy:becameInactive` with an angular.element wrapped element as first argument and the element being spied on as second. This is nice to have if you want the URL bar to reflect where on the page the visitor are, like this: 262 | 263 | ```js 264 | angular.module('myApp', ['duScroll']). 265 | run(function($rootScope) { 266 | if(!window.history || !history.replaceState) { 267 | return; 268 | } 269 | $rootScope.$on('duScrollspy:becameActive', function($event, $element, $target){ 270 | //Automaticly update location 271 | var hash = $element.prop('hash'); 272 | if (hash) { 273 | history.replaceState(null, null, hash); 274 | } 275 | }); 276 | }); 277 | ``` 278 | 279 | 280 | Building 281 | -------- 282 | 283 | $ npm install 284 | $ bower install 285 | $ gulp 286 | 287 | Tests 288 | ----- 289 | 290 | ### Unit tests 291 | 292 | $ npm test 293 | 294 | ### End to end tests 295 | 296 | $ npm run update-webdriver 297 | $ npm run protractor 298 | -------------------------------------------------------------------------------- /angular-scroll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * x is a value between 0 and 1, indicating where in the animation you are. 3 | */ 4 | var duScrollDefaultEasing = function (x) { 5 | 'use strict'; 6 | 7 | if(x < 0.5) { 8 | return Math.pow(x*2, 2)/2; 9 | } 10 | return 1-Math.pow((1-x)*2, 2)/2; 11 | }; 12 | 13 | var duScroll = angular.module('duScroll', [ 14 | 'duScroll.scrollspy', 15 | 'duScroll.smoothScroll', 16 | 'duScroll.scrollContainer', 17 | 'duScroll.spyContext', 18 | 'duScroll.scrollHelpers' 19 | ]) 20 | //Default animation duration for smoothScroll directive 21 | .value('duScrollDuration', 350) 22 | //Scrollspy debounce interval, set to 0 to disable 23 | .value('duScrollSpyWait', 100) 24 | //Scrollspy forced refresh interval, use if your content changes or reflows without scrolling. 25 | //0 to disable 26 | .value('duScrollSpyRefreshInterval', 0) 27 | //Wether or not multiple scrollspies can be active at once 28 | .value('duScrollGreedy', false) 29 | //Default offset for smoothScroll directive 30 | .value('duScrollOffset', 0) 31 | //Default easing function for scroll animation 32 | .value('duScrollEasing', duScrollDefaultEasing) 33 | //Which events on the container (such as body) should cancel scroll animations 34 | .value('duScrollCancelOnEvents', 'scroll mousedown mousewheel touchmove keydown') 35 | //Whether or not to activate the last scrollspy, when page/container bottom is reached 36 | .value('duScrollBottomSpy', false) 37 | //Active class name 38 | .value('duScrollActiveClass', 'active'); 39 | 40 | if (typeof module !== 'undefined' && module && module.exports) { 41 | module.exports = duScroll; 42 | } 43 | 44 | 45 | angular.module('duScroll.scrollHelpers', ['duScroll.requestAnimation']) 46 | .run(["$window", "$q", "cancelAnimation", "requestAnimation", "duScrollEasing", "duScrollDuration", "duScrollOffset", "duScrollCancelOnEvents", function($window, $q, cancelAnimation, requestAnimation, duScrollEasing, duScrollDuration, duScrollOffset, duScrollCancelOnEvents) { 47 | 'use strict'; 48 | 49 | var proto = {}; 50 | 51 | var isDocument = function(el) { 52 | return (typeof HTMLDocument !== 'undefined' && el instanceof HTMLDocument) || (el.nodeType && el.nodeType === el.DOCUMENT_NODE); 53 | }; 54 | 55 | var isElement = function(el) { 56 | return (typeof HTMLElement !== 'undefined' && el instanceof HTMLElement) || (el.nodeType && el.nodeType === el.ELEMENT_NODE); 57 | }; 58 | 59 | var unwrap = function(el) { 60 | return isElement(el) || isDocument(el) ? el : el[0]; 61 | }; 62 | 63 | proto.duScrollTo = function(left, top, duration, easing) { 64 | var aliasFn; 65 | if(angular.isElement(left)) { 66 | aliasFn = this.duScrollToElement; 67 | } else if(angular.isDefined(duration)) { 68 | aliasFn = this.duScrollToAnimated; 69 | } 70 | if(aliasFn) { 71 | return aliasFn.apply(this, arguments); 72 | } 73 | var el = unwrap(this); 74 | if(isDocument(el)) { 75 | return $window.scrollTo(left, top); 76 | } 77 | el.scrollLeft = left; 78 | el.scrollTop = top; 79 | }; 80 | 81 | var scrollAnimation, deferred; 82 | proto.duScrollToAnimated = function(left, top, duration, easing) { 83 | if(duration && !easing) { 84 | easing = duScrollEasing; 85 | } 86 | var startLeft = this.duScrollLeft(), 87 | startTop = this.duScrollTop(), 88 | deltaLeft = Math.round(left - startLeft), 89 | deltaTop = Math.round(top - startTop); 90 | 91 | var startTime = null, progress = 0; 92 | var el = this; 93 | 94 | var cancelScrollAnimation = function($event) { 95 | if (!$event || (progress && $event.which > 0)) { 96 | if(duScrollCancelOnEvents) { 97 | el.unbind(duScrollCancelOnEvents, cancelScrollAnimation); 98 | } 99 | cancelAnimation(scrollAnimation); 100 | deferred.reject(); 101 | scrollAnimation = null; 102 | } 103 | }; 104 | 105 | if(scrollAnimation) { 106 | cancelScrollAnimation(); 107 | } 108 | deferred = $q.defer(); 109 | 110 | if(duration === 0 || (!deltaLeft && !deltaTop)) { 111 | if(duration === 0) { 112 | el.duScrollTo(left, top); 113 | } 114 | deferred.resolve(); 115 | return deferred.promise; 116 | } 117 | 118 | var animationStep = function(timestamp) { 119 | if (startTime === null) { 120 | startTime = timestamp; 121 | } 122 | 123 | progress = timestamp - startTime; 124 | var percent = (progress >= duration ? 1 : easing(progress/duration)); 125 | 126 | el.scrollTo( 127 | startLeft + Math.ceil(deltaLeft * percent), 128 | startTop + Math.ceil(deltaTop * percent) 129 | ); 130 | if(percent < 1) { 131 | scrollAnimation = requestAnimation(animationStep); 132 | } else { 133 | if(duScrollCancelOnEvents) { 134 | el.unbind(duScrollCancelOnEvents, cancelScrollAnimation); 135 | } 136 | scrollAnimation = null; 137 | deferred.resolve(); 138 | } 139 | }; 140 | 141 | //Fix random mobile safari bug when scrolling to top by hitting status bar 142 | el.duScrollTo(startLeft, startTop); 143 | 144 | if(duScrollCancelOnEvents) { 145 | el.bind(duScrollCancelOnEvents, cancelScrollAnimation); 146 | } 147 | 148 | scrollAnimation = requestAnimation(animationStep); 149 | return deferred.promise; 150 | }; 151 | 152 | proto.duScrollToElement = function(target, offset, duration, easing) { 153 | var el = unwrap(this); 154 | if(!angular.isNumber(offset) || isNaN(offset)) { 155 | offset = duScrollOffset; 156 | } 157 | var top = this.duScrollTop() + unwrap(target).getBoundingClientRect().top - offset; 158 | if(isElement(el)) { 159 | top -= el.getBoundingClientRect().top; 160 | } 161 | return this.duScrollTo(0, top, duration, easing); 162 | }; 163 | 164 | proto.duScrollLeft = function(value, duration, easing) { 165 | if(angular.isNumber(value)) { 166 | return this.duScrollTo(value, this.duScrollTop(), duration, easing); 167 | } 168 | var el = unwrap(this); 169 | if(isDocument(el)) { 170 | return $window.scrollX || document.documentElement.scrollLeft || document.body.scrollLeft; 171 | } 172 | return el.scrollLeft; 173 | }; 174 | proto.duScrollTop = function(value, duration, easing) { 175 | if(angular.isNumber(value)) { 176 | return this.duScrollTo(this.duScrollLeft(), value, duration, easing); 177 | } 178 | var el = unwrap(this); 179 | if(isDocument(el)) { 180 | return $window.scrollY || document.documentElement.scrollTop || document.body.scrollTop; 181 | } 182 | return el.scrollTop; 183 | }; 184 | 185 | proto.duScrollToElementAnimated = function(target, offset, duration, easing) { 186 | return this.duScrollToElement(target, offset, duration || duScrollDuration, easing); 187 | }; 188 | 189 | proto.duScrollTopAnimated = function(top, duration, easing) { 190 | return this.duScrollTop(top, duration || duScrollDuration, easing); 191 | }; 192 | 193 | proto.duScrollLeftAnimated = function(left, duration, easing) { 194 | return this.duScrollLeft(left, duration || duScrollDuration, easing); 195 | }; 196 | 197 | angular.forEach(proto, function(fn, key) { 198 | angular.element.prototype[key] = fn; 199 | 200 | //Remove prefix if not already claimed by jQuery / ui.utils 201 | var unprefixed = key.replace(/^duScroll/, 'scroll'); 202 | if(angular.isUndefined(angular.element.prototype[unprefixed])) { 203 | angular.element.prototype[unprefixed] = fn; 204 | } 205 | }); 206 | 207 | }]); 208 | 209 | 210 | //Adapted from https://gist.github.com/paulirish/1579671 211 | angular.module('duScroll.polyfill', []) 212 | .factory('polyfill', ["$window", function($window) { 213 | 'use strict'; 214 | 215 | var vendors = ['webkit', 'moz', 'o', 'ms']; 216 | 217 | return function(fnName, fallback) { 218 | if($window[fnName]) { 219 | return $window[fnName]; 220 | } 221 | var suffix = fnName.substr(0, 1).toUpperCase() + fnName.substr(1); 222 | for(var key, i = 0; i < vendors.length; i++) { 223 | key = vendors[i]+suffix; 224 | if($window[key]) { 225 | return $window[key]; 226 | } 227 | } 228 | return fallback; 229 | }; 230 | }]); 231 | 232 | angular.module('duScroll.requestAnimation', ['duScroll.polyfill']) 233 | .factory('requestAnimation', ["polyfill", "$timeout", function(polyfill, $timeout) { 234 | 'use strict'; 235 | 236 | var lastTime = 0; 237 | var fallback = function(callback, element) { 238 | var currTime = new Date().getTime(); 239 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 240 | var id = $timeout(function() { callback(currTime + timeToCall); }, 241 | timeToCall); 242 | lastTime = currTime + timeToCall; 243 | return id; 244 | }; 245 | 246 | return polyfill('requestAnimationFrame', fallback); 247 | }]) 248 | .factory('cancelAnimation', ["polyfill", "$timeout", function(polyfill, $timeout) { 249 | 'use strict'; 250 | 251 | var fallback = function(promise) { 252 | $timeout.cancel(promise); 253 | }; 254 | 255 | return polyfill('cancelAnimationFrame', fallback); 256 | }]); 257 | 258 | 259 | angular.module('duScroll.spyAPI', ['duScroll.scrollContainerAPI']) 260 | .factory('spyAPI', ["$rootScope", "$timeout", "$interval", "$window", "$document", "scrollContainerAPI", "duScrollGreedy", "duScrollSpyWait", "duScrollSpyRefreshInterval", "duScrollBottomSpy", "duScrollActiveClass", function($rootScope, $timeout, $interval, $window, $document, scrollContainerAPI, duScrollGreedy, duScrollSpyWait, duScrollSpyRefreshInterval, duScrollBottomSpy, duScrollActiveClass) { 261 | 'use strict'; 262 | 263 | var createScrollHandler = function(context) { 264 | var timer = false, queued = false; 265 | var handler = function() { 266 | queued = false; 267 | var container = context.container, 268 | containerEl = container[0], 269 | containerOffset = 0, 270 | bottomReached; 271 | 272 | if (typeof HTMLElement !== 'undefined' && containerEl instanceof HTMLElement || containerEl.nodeType && containerEl.nodeType === containerEl.ELEMENT_NODE) { 273 | containerOffset = containerEl.getBoundingClientRect().top; 274 | bottomReached = Math.round(containerEl.scrollTop + containerEl.clientHeight) >= containerEl.scrollHeight; 275 | } else { 276 | var documentScrollHeight = $document[0].body.scrollHeight || $document[0].documentElement.scrollHeight; // documentElement for IE11 277 | bottomReached = Math.round($window.pageYOffset + $window.innerHeight) >= documentScrollHeight; 278 | } 279 | var compareProperty = (duScrollBottomSpy && bottomReached ? 'bottom' : 'top'); 280 | 281 | var i, currentlyActive, toBeActive, spies, spy, pos; 282 | spies = context.spies; 283 | currentlyActive = context.currentlyActive; 284 | toBeActive = undefined; 285 | 286 | for(i = 0; i < spies.length; i++) { 287 | spy = spies[i]; 288 | pos = spy.getTargetPosition(); 289 | if (!pos || !spy.$element) continue; 290 | 291 | if((duScrollBottomSpy && bottomReached) || (pos.top + spy.offset - containerOffset < 20 && (duScrollGreedy || pos.top*-1 + containerOffset) < pos.height)) { 292 | //Find the one closest the viewport top or the page bottom if it's reached 293 | if(!toBeActive || toBeActive[compareProperty] < pos[compareProperty]) { 294 | toBeActive = { 295 | spy: spy 296 | }; 297 | toBeActive[compareProperty] = pos[compareProperty]; 298 | } 299 | } 300 | } 301 | 302 | if(toBeActive) { 303 | toBeActive = toBeActive.spy; 304 | } 305 | if(currentlyActive === toBeActive || (duScrollGreedy && !toBeActive)) return; 306 | if(currentlyActive && currentlyActive.$element) { 307 | currentlyActive.$element.removeClass(duScrollActiveClass); 308 | $rootScope.$broadcast( 309 | 'duScrollspy:becameInactive', 310 | currentlyActive.$element, 311 | angular.element(currentlyActive.getTargetElement()) 312 | ); 313 | } 314 | if(toBeActive) { 315 | toBeActive.$element.addClass(duScrollActiveClass); 316 | $rootScope.$broadcast( 317 | 'duScrollspy:becameActive', 318 | toBeActive.$element, 319 | angular.element(toBeActive.getTargetElement()) 320 | ); 321 | } 322 | context.currentlyActive = toBeActive; 323 | }; 324 | 325 | if(!duScrollSpyWait) { 326 | return handler; 327 | } 328 | 329 | //Debounce for potential performance savings 330 | return function() { 331 | if(!timer) { 332 | handler(); 333 | timer = $timeout(function() { 334 | timer = false; 335 | if(queued) { 336 | handler(); 337 | } 338 | }, duScrollSpyWait, false); 339 | } else { 340 | queued = true; 341 | } 342 | }; 343 | }; 344 | 345 | var contexts = {}; 346 | 347 | var createContext = function($scope) { 348 | var id = $scope.$id; 349 | var context = { 350 | spies: [] 351 | }; 352 | 353 | context.handler = createScrollHandler(context); 354 | contexts[id] = context; 355 | 356 | $scope.$on('$destroy', function() { 357 | destroyContext($scope); 358 | }); 359 | 360 | return id; 361 | }; 362 | 363 | var destroyContext = function($scope) { 364 | var id = $scope.$id; 365 | var context = contexts[id], container = context.container; 366 | if(context.intervalPromise) { 367 | $interval.cancel(context.intervalPromise); 368 | } 369 | if(container) { 370 | container.off('scroll', context.handler); 371 | } 372 | delete contexts[id]; 373 | }; 374 | 375 | var defaultContextId = createContext($rootScope); 376 | 377 | var getContextForScope = function(scope) { 378 | if(contexts[scope.$id]) { 379 | return contexts[scope.$id]; 380 | } 381 | if(scope.$parent) { 382 | return getContextForScope(scope.$parent); 383 | } 384 | return contexts[defaultContextId]; 385 | }; 386 | 387 | var getContextForSpy = function(spy) { 388 | var context, contextId, scope = spy.$scope; 389 | if(scope) { 390 | return getContextForScope(scope); 391 | } 392 | //No scope, most likely destroyed 393 | for(contextId in contexts) { 394 | context = contexts[contextId]; 395 | if(context.spies.indexOf(spy) !== -1) { 396 | return context; 397 | } 398 | } 399 | }; 400 | 401 | var isElementInDocument = function(element) { 402 | while (element.parentNode) { 403 | element = element.parentNode; 404 | if (element === document) { 405 | return true; 406 | } 407 | } 408 | return false; 409 | }; 410 | 411 | var addSpy = function(spy) { 412 | var context = getContextForSpy(spy); 413 | if (!context) return; 414 | context.spies.push(spy); 415 | if (!context.container || !isElementInDocument(context.container)) { 416 | if(context.container) { 417 | context.container.off('scroll', context.handler); 418 | } 419 | context.container = scrollContainerAPI.getContainer(spy.$scope); 420 | if (duScrollSpyRefreshInterval && !context.intervalPromise) { 421 | context.intervalPromise = $interval(context.handler, duScrollSpyRefreshInterval, 0, false); 422 | } 423 | context.container.on('scroll', context.handler).triggerHandler('scroll'); 424 | } 425 | }; 426 | 427 | var removeSpy = function(spy) { 428 | var context = getContextForSpy(spy); 429 | if(spy === context.currentlyActive) { 430 | $rootScope.$broadcast('duScrollspy:becameInactive', context.currentlyActive.$element); 431 | context.currentlyActive = null; 432 | } 433 | var i = context.spies.indexOf(spy); 434 | if(i !== -1) { 435 | context.spies.splice(i, 1); 436 | } 437 | spy.$element = null; 438 | }; 439 | 440 | return { 441 | addSpy: addSpy, 442 | removeSpy: removeSpy, 443 | createContext: createContext, 444 | destroyContext: destroyContext, 445 | getContextForScope: getContextForScope 446 | }; 447 | }]); 448 | 449 | 450 | angular.module('duScroll.scrollContainerAPI', []) 451 | .factory('scrollContainerAPI', ["$document", function($document) { 452 | 'use strict'; 453 | 454 | var containers = {}; 455 | 456 | var setContainer = function(scope, element) { 457 | var id = scope.$id; 458 | containers[id] = element; 459 | return id; 460 | }; 461 | 462 | var getContainerId = function(scope) { 463 | if(containers[scope.$id]) { 464 | return scope.$id; 465 | } 466 | if(scope.$parent) { 467 | return getContainerId(scope.$parent); 468 | } 469 | return; 470 | }; 471 | 472 | var getContainer = function(scope) { 473 | var id = getContainerId(scope); 474 | return id ? containers[id] : $document; 475 | }; 476 | 477 | var removeContainer = function(scope) { 478 | var id = getContainerId(scope); 479 | if(id) { 480 | delete containers[id]; 481 | } 482 | }; 483 | 484 | return { 485 | getContainerId: getContainerId, 486 | getContainer: getContainer, 487 | setContainer: setContainer, 488 | removeContainer: removeContainer 489 | }; 490 | }]); 491 | 492 | 493 | angular.module('duScroll.smoothScroll', ['duScroll.scrollHelpers', 'duScroll.scrollContainerAPI']) 494 | .directive('duSmoothScroll', ["duScrollDuration", "duScrollOffset", "scrollContainerAPI", function(duScrollDuration, duScrollOffset, scrollContainerAPI) { 495 | 'use strict'; 496 | 497 | return { 498 | link : function($scope, $element, $attr) { 499 | $element.on('click', function(e) { 500 | if((!$attr.href || $attr.href.indexOf('#') === -1) && $attr.duSmoothScroll === '') return; 501 | 502 | var id = $attr.href ? $attr.href.replace(/.*(?=#[^\s]+$)/, '').substring(1) : $attr.duSmoothScroll; 503 | 504 | var target = document.getElementById(id) || document.getElementsByName(id)[0]; 505 | if(!target || !target.getBoundingClientRect) return; 506 | 507 | if (e.stopPropagation) e.stopPropagation(); 508 | if (e.preventDefault) e.preventDefault(); 509 | 510 | var offset = $attr.offset ? parseInt($attr.offset, 10) : duScrollOffset; 511 | var duration = $attr.duration ? parseInt($attr.duration, 10) : duScrollDuration; 512 | var container = scrollContainerAPI.getContainer($scope); 513 | 514 | container.duScrollToElement( 515 | angular.element(target), 516 | isNaN(offset) ? 0 : offset, 517 | isNaN(duration) ? 0 : duration 518 | ); 519 | }); 520 | } 521 | }; 522 | }]); 523 | 524 | 525 | angular.module('duScroll.spyContext', ['duScroll.spyAPI']) 526 | .directive('duSpyContext', ["spyAPI", function(spyAPI) { 527 | 'use strict'; 528 | 529 | return { 530 | restrict: 'A', 531 | scope: true, 532 | compile: function compile(tElement, tAttrs, transclude) { 533 | return { 534 | pre: function preLink($scope, iElement, iAttrs, controller) { 535 | spyAPI.createContext($scope); 536 | } 537 | }; 538 | } 539 | }; 540 | }]); 541 | 542 | 543 | angular.module('duScroll.scrollContainer', ['duScroll.scrollContainerAPI']) 544 | .directive('duScrollContainer', ["scrollContainerAPI", function(scrollContainerAPI){ 545 | 'use strict'; 546 | 547 | return { 548 | restrict: 'A', 549 | scope: true, 550 | compile: function compile(tElement, tAttrs, transclude) { 551 | return { 552 | pre: function preLink($scope, iElement, iAttrs, controller) { 553 | iAttrs.$observe('duScrollContainer', function(element) { 554 | if(angular.isString(element)) { 555 | element = document.getElementById(element); 556 | } 557 | 558 | element = (angular.isElement(element) ? angular.element(element) : iElement); 559 | scrollContainerAPI.setContainer($scope, element); 560 | $scope.$on('$destroy', function() { 561 | scrollContainerAPI.removeContainer($scope); 562 | }); 563 | }); 564 | } 565 | }; 566 | } 567 | }; 568 | }]); 569 | 570 | 571 | angular.module('duScroll.scrollspy', ['duScroll.spyAPI']) 572 | .directive('duScrollspy', ["spyAPI", "duScrollOffset", "$timeout", "$rootScope", function(spyAPI, duScrollOffset, $timeout, $rootScope) { 573 | 'use strict'; 574 | 575 | var Spy = function(targetElementOrId, $scope, $element, offset) { 576 | if(angular.isElement(targetElementOrId)) { 577 | this.target = targetElementOrId; 578 | } else if(angular.isString(targetElementOrId)) { 579 | this.targetId = targetElementOrId; 580 | } 581 | this.$scope = $scope; 582 | this.$element = $element; 583 | this.offset = offset; 584 | }; 585 | 586 | Spy.prototype.getTargetElement = function() { 587 | if (!this.target && this.targetId) { 588 | this.target = document.getElementById(this.targetId) || document.getElementsByName(this.targetId)[0]; 589 | } 590 | return this.target; 591 | }; 592 | 593 | Spy.prototype.getTargetPosition = function() { 594 | var target = this.getTargetElement(); 595 | if(target) { 596 | return target.getBoundingClientRect(); 597 | } 598 | }; 599 | 600 | Spy.prototype.flushTargetCache = function() { 601 | if(this.targetId) { 602 | this.target = undefined; 603 | } 604 | }; 605 | 606 | return { 607 | link: function ($scope, $element, $attr) { 608 | var href = $attr.ngHref || $attr.href; 609 | var targetId; 610 | 611 | if (href && href.indexOf('#') !== -1) { 612 | targetId = href.replace(/.*(?=#[^\s]+$)/, '').substring(1); 613 | } else if($attr.duScrollspy) { 614 | targetId = $attr.duScrollspy; 615 | } else if($attr.duSmoothScroll) { 616 | targetId = $attr.duSmoothScroll; 617 | } 618 | if(!targetId) return; 619 | 620 | // Run this in the next execution loop so that the scroll context has a chance 621 | // to initialize 622 | var timeoutPromise = $timeout(function() { 623 | var spy = new Spy(targetId, $scope, $element, -($attr.offset ? parseInt($attr.offset, 10) : duScrollOffset)); 624 | spyAPI.addSpy(spy); 625 | 626 | $scope.$on('$locationChangeSuccess', spy.flushTargetCache.bind(spy)); 627 | var deregisterOnStateChange = $rootScope.$on('$stateChangeSuccess', spy.flushTargetCache.bind(spy)); 628 | $scope.$on('$destroy', function() { 629 | spyAPI.removeSpy(spy); 630 | deregisterOnStateChange(); 631 | }); 632 | }, 0, false); 633 | $scope.$on('$destroy', function() {$timeout.cancel(timeoutPromise);}); 634 | } 635 | }; 636 | }]); 637 | -------------------------------------------------------------------------------- /angular-scroll.min.js: -------------------------------------------------------------------------------- 1 | var duScrollDefaultEasing=function(e){"use strict";return e<.5?Math.pow(2*e,2)/2:1-Math.pow(2*(1-e),2)/2},duScroll=angular.module("duScroll",["duScroll.scrollspy","duScroll.smoothScroll","duScroll.scrollContainer","duScroll.spyContext","duScroll.scrollHelpers"]).value("duScrollDuration",350).value("duScrollSpyWait",100).value("duScrollSpyRefreshInterval",0).value("duScrollGreedy",!1).value("duScrollOffset",0).value("duScrollEasing",duScrollDefaultEasing).value("duScrollCancelOnEvents","scroll mousedown mousewheel touchmove keydown").value("duScrollBottomSpy",!1).value("duScrollActiveClass","active");"undefined"!=typeof module&&module&&module.exports&&(module.exports=duScroll),angular.module("duScroll.scrollHelpers",["duScroll.requestAnimation"]).run(["$window","$q","cancelAnimation","requestAnimation","duScrollEasing","duScrollDuration","duScrollOffset","duScrollCancelOnEvents",function(e,t,n,r,o,l,u,i){"use strict";var c={},a=function(e){return"undefined"!=typeof HTMLDocument&&e instanceof HTMLDocument||e.nodeType&&e.nodeType===e.DOCUMENT_NODE},s=function(e){return"undefined"!=typeof HTMLElement&&e instanceof HTMLElement||e.nodeType&&e.nodeType===e.ELEMENT_NODE},d=function(e){return s(e)||a(e)?e:e[0]};c.duScrollTo=function(t,n,r,o){var l;if(angular.isElement(t)?l=this.duScrollToElement:angular.isDefined(r)&&(l=this.duScrollToAnimated),l)return l.apply(this,arguments);var u=d(this);return a(u)?e.scrollTo(t,n):(u.scrollLeft=t,void(u.scrollTop=n))};var f,m;c.duScrollToAnimated=function(e,l,u,c){u&&!c&&(c=o);var a=this.duScrollLeft(),s=this.duScrollTop(),d=Math.round(e-a),p=Math.round(l-s),S=null,g=0,v=this,h=function(e){(!e||g&&e.which>0)&&(i&&v.unbind(i,h),n(f),m.reject(),f=null)};if(f&&h(),m=t.defer(),0===u||!d&&!p)return 0===u&&v.duScrollTo(e,l),m.resolve(),m.promise;var y=function(e){null===S&&(S=e),g=e-S;var t=g>=u?1:c(g/u);v.scrollTo(a+Math.ceil(d*t),s+Math.ceil(p*t)),t<1?f=r(y):(i&&v.unbind(i,h),f=null,m.resolve())};return v.duScrollTo(a,s),i&&v.bind(i,h),f=r(y),m.promise},c.duScrollToElement=function(e,t,n,r){var o=d(this);angular.isNumber(t)&&!isNaN(t)||(t=u);var l=this.duScrollTop()+d(e).getBoundingClientRect().top-t;return s(o)&&(l-=o.getBoundingClientRect().top),this.duScrollTo(0,l,n,r)},c.duScrollLeft=function(t,n,r){if(angular.isNumber(t))return this.duScrollTo(t,this.duScrollTop(),n,r);var o=d(this);return a(o)?e.scrollX||document.documentElement.scrollLeft||document.body.scrollLeft:o.scrollLeft},c.duScrollTop=function(t,n,r){if(angular.isNumber(t))return this.duScrollTo(this.duScrollLeft(),t,n,r);var o=d(this);return a(o)?e.scrollY||document.documentElement.scrollTop||document.body.scrollTop:o.scrollTop},c.duScrollToElementAnimated=function(e,t,n,r){return this.duScrollToElement(e,t,n||l,r)},c.duScrollTopAnimated=function(e,t,n){return this.duScrollTop(e,t||l,n)},c.duScrollLeftAnimated=function(e,t,n){return this.duScrollLeft(e,t||l,n)},angular.forEach(c,function(e,t){angular.element.prototype[t]=e;var n=t.replace(/^duScroll/,"scroll");angular.isUndefined(angular.element.prototype[n])&&(angular.element.prototype[n]=e)})}]),angular.module("duScroll.polyfill",[]).factory("polyfill",["$window",function(e){"use strict";var t=["webkit","moz","o","ms"];return function(n,r){if(e[n])return e[n];for(var o,l=n.substr(0,1).toUpperCase()+n.substr(1),u=0;u=i.scrollHeight;else{var f=o[0].body.scrollHeight||o[0].documentElement.scrollHeight;t=Math.round(r.pageYOffset+r.innerHeight)>=f}var m,p,S,g,v,h,y=a&&t?"bottom":"top";for(g=n.spies,p=n.currentlyActive,S=void 0,m=0;m 0)) {\n if(duScrollCancelOnEvents) {\n el.unbind(duScrollCancelOnEvents, cancelScrollAnimation);\n }\n cancelAnimation(scrollAnimation);\n deferred.reject();\n scrollAnimation = null;\n }\n };\n\n if(scrollAnimation) {\n cancelScrollAnimation();\n }\n deferred = $q.defer();\n\n if(duration === 0 || (!deltaLeft && !deltaTop)) {\n if(duration === 0) {\n el.duScrollTo(left, top);\n }\n deferred.resolve();\n return deferred.promise;\n }\n\n var animationStep = function(timestamp) {\n if (startTime === null) {\n startTime = timestamp;\n }\n\n progress = timestamp - startTime;\n var percent = (progress >= duration ? 1 : easing(progress/duration));\n\n el.scrollTo(\n startLeft + Math.ceil(deltaLeft * percent),\n startTop + Math.ceil(deltaTop * percent)\n );\n if(percent < 1) {\n scrollAnimation = requestAnimation(animationStep);\n } else {\n if(duScrollCancelOnEvents) {\n el.unbind(duScrollCancelOnEvents, cancelScrollAnimation);\n }\n scrollAnimation = null;\n deferred.resolve();\n }\n };\n\n //Fix random mobile safari bug when scrolling to top by hitting status bar\n el.duScrollTo(startLeft, startTop);\n\n if(duScrollCancelOnEvents) {\n el.bind(duScrollCancelOnEvents, cancelScrollAnimation);\n }\n\n scrollAnimation = requestAnimation(animationStep);\n return deferred.promise;\n };\n\n proto.duScrollToElement = function(target, offset, duration, easing) {\n var el = unwrap(this);\n if(!angular.isNumber(offset) || isNaN(offset)) {\n offset = duScrollOffset;\n }\n var top = this.duScrollTop() + unwrap(target).getBoundingClientRect().top - offset;\n if(isElement(el)) {\n top -= el.getBoundingClientRect().top;\n }\n return this.duScrollTo(0, top, duration, easing);\n };\n\n proto.duScrollLeft = function(value, duration, easing) {\n if(angular.isNumber(value)) {\n return this.duScrollTo(value, this.duScrollTop(), duration, easing);\n }\n var el = unwrap(this);\n if(isDocument(el)) {\n return $window.scrollX || document.documentElement.scrollLeft || document.body.scrollLeft;\n }\n return el.scrollLeft;\n };\n proto.duScrollTop = function(value, duration, easing) {\n if(angular.isNumber(value)) {\n return this.duScrollTo(this.duScrollLeft(), value, duration, easing);\n }\n var el = unwrap(this);\n if(isDocument(el)) {\n return $window.scrollY || document.documentElement.scrollTop || document.body.scrollTop;\n }\n return el.scrollTop;\n };\n\n proto.duScrollToElementAnimated = function(target, offset, duration, easing) {\n return this.duScrollToElement(target, offset, duration || duScrollDuration, easing);\n };\n\n proto.duScrollTopAnimated = function(top, duration, easing) {\n return this.duScrollTop(top, duration || duScrollDuration, easing);\n };\n\n proto.duScrollLeftAnimated = function(left, duration, easing) {\n return this.duScrollLeft(left, duration || duScrollDuration, easing);\n };\n\n angular.forEach(proto, function(fn, key) {\n angular.element.prototype[key] = fn;\n\n //Remove prefix if not already claimed by jQuery / ui.utils\n var unprefixed = key.replace(/^duScroll/, 'scroll');\n if(angular.isUndefined(angular.element.prototype[unprefixed])) {\n angular.element.prototype[unprefixed] = fn;\n }\n });\n\n});\n","//Adapted from https://gist.github.com/paulirish/1579671\nangular.module('duScroll.polyfill', [])\n.factory('polyfill', function($window) {\n 'use strict';\n\n var vendors = ['webkit', 'moz', 'o', 'ms'];\n\n return function(fnName, fallback) {\n if($window[fnName]) {\n return $window[fnName];\n }\n var suffix = fnName.substr(0, 1).toUpperCase() + fnName.substr(1);\n for(var key, i = 0; i < vendors.length; i++) {\n key = vendors[i]+suffix;\n if($window[key]) {\n return $window[key];\n }\n }\n return fallback;\n };\n});\n\nangular.module('duScroll.requestAnimation', ['duScroll.polyfill'])\n.factory('requestAnimation', function(polyfill, $timeout) {\n 'use strict';\n\n var lastTime = 0;\n var fallback = function(callback, element) {\n var currTime = new Date().getTime();\n var timeToCall = Math.max(0, 16 - (currTime - lastTime));\n var id = $timeout(function() { callback(currTime + timeToCall); },\n timeToCall);\n lastTime = currTime + timeToCall;\n return id;\n };\n\n return polyfill('requestAnimationFrame', fallback);\n})\n.factory('cancelAnimation', function(polyfill, $timeout) {\n 'use strict';\n\n var fallback = function(promise) {\n $timeout.cancel(promise);\n };\n\n return polyfill('cancelAnimationFrame', fallback);\n});\n","angular.module('duScroll.spyAPI', ['duScroll.scrollContainerAPI'])\n.factory('spyAPI', function($rootScope, $timeout, $interval, $window, $document, scrollContainerAPI, duScrollGreedy, duScrollSpyWait, duScrollSpyRefreshInterval, duScrollBottomSpy, duScrollActiveClass) {\n 'use strict';\n\n var createScrollHandler = function(context) {\n var timer = false, queued = false;\n var handler = function() {\n queued = false;\n var container = context.container,\n containerEl = container[0],\n containerOffset = 0,\n bottomReached;\n\n if (typeof HTMLElement !== 'undefined' && containerEl instanceof HTMLElement || containerEl.nodeType && containerEl.nodeType === containerEl.ELEMENT_NODE) {\n containerOffset = containerEl.getBoundingClientRect().top;\n bottomReached = Math.round(containerEl.scrollTop + containerEl.clientHeight) >= containerEl.scrollHeight;\n } else {\n var documentScrollHeight = $document[0].body.scrollHeight || $document[0].documentElement.scrollHeight; // documentElement for IE11\n bottomReached = Math.round($window.pageYOffset + $window.innerHeight) >= documentScrollHeight;\n }\n var compareProperty = (duScrollBottomSpy && bottomReached ? 'bottom' : 'top');\n\n var i, currentlyActive, toBeActive, spies, spy, pos;\n spies = context.spies;\n currentlyActive = context.currentlyActive;\n toBeActive = undefined;\n\n for(i = 0; i < spies.length; i++) {\n spy = spies[i];\n pos = spy.getTargetPosition();\n if (!pos || !spy.$element) continue;\n\n if((duScrollBottomSpy && bottomReached) || (pos.top + spy.offset - containerOffset < 20 && (duScrollGreedy || pos.top*-1 + containerOffset) < pos.height)) {\n //Find the one closest the viewport top or the page bottom if it's reached\n if(!toBeActive || toBeActive[compareProperty] < pos[compareProperty]) {\n toBeActive = {\n spy: spy\n };\n toBeActive[compareProperty] = pos[compareProperty];\n }\n }\n }\n\n if(toBeActive) {\n toBeActive = toBeActive.spy;\n }\n if(currentlyActive === toBeActive || (duScrollGreedy && !toBeActive)) return;\n if(currentlyActive && currentlyActive.$element) {\n currentlyActive.$element.removeClass(duScrollActiveClass);\n $rootScope.$broadcast(\n 'duScrollspy:becameInactive',\n currentlyActive.$element,\n angular.element(currentlyActive.getTargetElement())\n );\n }\n if(toBeActive) {\n toBeActive.$element.addClass(duScrollActiveClass);\n $rootScope.$broadcast(\n 'duScrollspy:becameActive',\n toBeActive.$element,\n angular.element(toBeActive.getTargetElement())\n );\n }\n context.currentlyActive = toBeActive;\n };\n\n if(!duScrollSpyWait) {\n return handler;\n }\n\n //Debounce for potential performance savings\n return function() {\n if(!timer) {\n handler();\n timer = $timeout(function() {\n timer = false;\n if(queued) {\n handler();\n }\n }, duScrollSpyWait, false);\n } else {\n queued = true;\n }\n };\n };\n\n var contexts = {};\n\n var createContext = function($scope) {\n var id = $scope.$id;\n var context = {\n spies: []\n };\n\n context.handler = createScrollHandler(context);\n contexts[id] = context;\n\n $scope.$on('$destroy', function() {\n destroyContext($scope);\n });\n\n return id;\n };\n\n var destroyContext = function($scope) {\n var id = $scope.$id;\n var context = contexts[id], container = context.container;\n if(context.intervalPromise) {\n $interval.cancel(context.intervalPromise);\n }\n if(container) {\n container.off('scroll', context.handler);\n }\n delete contexts[id];\n };\n\n var defaultContextId = createContext($rootScope);\n\n var getContextForScope = function(scope) {\n if(contexts[scope.$id]) {\n return contexts[scope.$id];\n }\n if(scope.$parent) {\n return getContextForScope(scope.$parent);\n }\n return contexts[defaultContextId];\n };\n\n var getContextForSpy = function(spy) {\n var context, contextId, scope = spy.$scope;\n if(scope) {\n return getContextForScope(scope);\n }\n //No scope, most likely destroyed\n for(contextId in contexts) {\n context = contexts[contextId];\n if(context.spies.indexOf(spy) !== -1) {\n return context;\n }\n }\n };\n\n var isElementInDocument = function(element) {\n while (element.parentNode) {\n element = element.parentNode;\n if (element === document) {\n return true;\n }\n }\n return false;\n };\n\n var addSpy = function(spy) {\n var context = getContextForSpy(spy);\n if (!context) return;\n context.spies.push(spy);\n if (!context.container || !isElementInDocument(context.container)) {\n if(context.container) {\n context.container.off('scroll', context.handler);\n }\n context.container = scrollContainerAPI.getContainer(spy.$scope);\n if (duScrollSpyRefreshInterval && !context.intervalPromise) {\n context.intervalPromise = $interval(context.handler, duScrollSpyRefreshInterval, 0, false);\n }\n context.container.on('scroll', context.handler).triggerHandler('scroll');\n }\n };\n\n var removeSpy = function(spy) {\n var context = getContextForSpy(spy);\n if(spy === context.currentlyActive) {\n $rootScope.$broadcast('duScrollspy:becameInactive', context.currentlyActive.$element);\n context.currentlyActive = null;\n }\n var i = context.spies.indexOf(spy);\n if(i !== -1) {\n context.spies.splice(i, 1);\n }\n\t\tspy.$element = null;\n };\n\n return {\n addSpy: addSpy,\n removeSpy: removeSpy,\n createContext: createContext,\n destroyContext: destroyContext,\n getContextForScope: getContextForScope\n };\n});\n","angular.module('duScroll.scrollContainerAPI', [])\n.factory('scrollContainerAPI', function($document) {\n 'use strict';\n\n var containers = {};\n\n var setContainer = function(scope, element) {\n var id = scope.$id;\n containers[id] = element;\n return id;\n };\n\n var getContainerId = function(scope) {\n if(containers[scope.$id]) {\n return scope.$id;\n }\n if(scope.$parent) {\n return getContainerId(scope.$parent);\n }\n return;\n };\n\n var getContainer = function(scope) {\n var id = getContainerId(scope);\n return id ? containers[id] : $document;\n };\n\n var removeContainer = function(scope) {\n var id = getContainerId(scope);\n if(id) {\n delete containers[id];\n }\n };\n\n return {\n getContainerId: getContainerId,\n getContainer: getContainer,\n setContainer: setContainer,\n removeContainer: removeContainer\n };\n});\n","angular.module('duScroll.smoothScroll', ['duScroll.scrollHelpers', 'duScroll.scrollContainerAPI'])\n.directive('duSmoothScroll', function(duScrollDuration, duScrollOffset, scrollContainerAPI) {\n 'use strict';\n\n return {\n link : function($scope, $element, $attr) {\n $element.on('click', function(e) {\n if((!$attr.href || $attr.href.indexOf('#') === -1) && $attr.duSmoothScroll === '') return;\n\n var id = $attr.href ? $attr.href.replace(/.*(?=#[^\\s]+$)/, '').substring(1) : $attr.duSmoothScroll;\n\n var target = document.getElementById(id) || document.getElementsByName(id)[0];\n if(!target || !target.getBoundingClientRect) return;\n\n if (e.stopPropagation) e.stopPropagation();\n if (e.preventDefault) e.preventDefault();\n\n var offset = $attr.offset ? parseInt($attr.offset, 10) : duScrollOffset;\n var duration = $attr.duration ? parseInt($attr.duration, 10) : duScrollDuration;\n var container = scrollContainerAPI.getContainer($scope);\n\n container.duScrollToElement(\n angular.element(target),\n isNaN(offset) ? 0 : offset,\n isNaN(duration) ? 0 : duration\n );\n });\n }\n };\n});\n","angular.module('duScroll.spyContext', ['duScroll.spyAPI'])\n.directive('duSpyContext', function(spyAPI) {\n 'use strict';\n\n return {\n restrict: 'A',\n scope: true,\n compile: function compile(tElement, tAttrs, transclude) {\n return {\n pre: function preLink($scope, iElement, iAttrs, controller) {\n spyAPI.createContext($scope);\n }\n };\n }\n };\n});\n","angular.module('duScroll.scrollContainer', ['duScroll.scrollContainerAPI'])\n.directive('duScrollContainer', function(scrollContainerAPI){\n 'use strict';\n\n return {\n restrict: 'A',\n scope: true,\n compile: function compile(tElement, tAttrs, transclude) {\n return {\n pre: function preLink($scope, iElement, iAttrs, controller) {\n iAttrs.$observe('duScrollContainer', function(element) {\n if(angular.isString(element)) {\n element = document.getElementById(element);\n }\n\n element = (angular.isElement(element) ? angular.element(element) : iElement);\n scrollContainerAPI.setContainer($scope, element);\n $scope.$on('$destroy', function() {\n scrollContainerAPI.removeContainer($scope);\n });\n });\n }\n };\n }\n };\n});\n","angular.module('duScroll.scrollspy', ['duScroll.spyAPI'])\n.directive('duScrollspy', function(spyAPI, duScrollOffset, $timeout, $rootScope) {\n 'use strict';\n\n var Spy = function(targetElementOrId, $scope, $element, offset) {\n if(angular.isElement(targetElementOrId)) {\n this.target = targetElementOrId;\n } else if(angular.isString(targetElementOrId)) {\n this.targetId = targetElementOrId;\n }\n this.$scope = $scope;\n this.$element = $element;\n this.offset = offset;\n };\n\n Spy.prototype.getTargetElement = function() {\n if (!this.target && this.targetId) {\n this.target = document.getElementById(this.targetId) || document.getElementsByName(this.targetId)[0];\n }\n return this.target;\n };\n\n Spy.prototype.getTargetPosition = function() {\n var target = this.getTargetElement();\n if(target) {\n return target.getBoundingClientRect();\n }\n };\n\n Spy.prototype.flushTargetCache = function() {\n if(this.targetId) {\n this.target = undefined;\n }\n };\n\n return {\n link: function ($scope, $element, $attr) {\n var href = $attr.ngHref || $attr.href;\n var targetId;\n\n if (href && href.indexOf('#') !== -1) {\n targetId = href.replace(/.*(?=#[^\\s]+$)/, '').substring(1);\n } else if($attr.duScrollspy) {\n targetId = $attr.duScrollspy;\n } else if($attr.duSmoothScroll) {\n targetId = $attr.duSmoothScroll;\n }\n if(!targetId) return;\n\n // Run this in the next execution loop so that the scroll context has a chance\n // to initialize\n var timeoutPromise = $timeout(function() {\n var spy = new Spy(targetId, $scope, $element, -($attr.offset ? parseInt($attr.offset, 10) : duScrollOffset));\n spyAPI.addSpy(spy);\n\n $scope.$on('$locationChangeSuccess', spy.flushTargetCache.bind(spy));\n var deregisterOnStateChange = $rootScope.$on('$stateChangeSuccess', spy.flushTargetCache.bind(spy));\n $scope.$on('$destroy', function() {\n spyAPI.removeSpy(spy);\n deregisterOnStateChange();\n });\n }, 0, false);\n $scope.$on('$destroy', function() {$timeout.cancel(timeoutPromise);});\n }\n };\n});\n"]} -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-scroll", 3 | "version": "1.0.0", 4 | "main": "angular-scroll.js", 5 | "ignore": [ 6 | "**/.*", 7 | "node_modules", 8 | "bower_components", 9 | "test", 10 | "tests", 11 | "package.json", 12 | "src", 13 | "Gruntfile.js" 14 | ], 15 | "dependencies": { 16 | "angular": "^1.2.16" 17 | }, 18 | "devDependencies": { 19 | "angular-mocks": "^1.2.16" 20 | }, 21 | "resolutions": { 22 | "angular": "^1.2.16" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/container.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular.js Container Scroll Example 7 | 8 | 84 | 85 | 86 |
87 | 95 |

Angular.js Container Scroll Example

96 |
97 |
98 |

Section 1

99 |

To section 4 Bacon ipsum dolor sit amet sausage tail capicola ground round hamburger ham hock. Short ribs pig andouille meatball, pastrami tri-tip fatback ham hock shank kielbasa swine. Rump pancetta jerky kielbasa doner beef ribs tongue hamburger strip steak drumstick andouille shoulder shank flank. Swine drumstick meatball pig beef sausage strip steak.

100 | 101 | 102 |
103 | 104 |
105 |

Section 2

106 |

Bacon strip steak ground round, tongue pastrami short ribs pork chop venison turducken sausage sirloin. Flank chicken pork chop capicola turkey turducken cow pork loin biltong meatball drumstick pancetta filet mignon ground round fatback. Ham hock jerky short ribs brisket. Meatloaf shoulder pork chop capicola, sirloin swine pig pork. Jerky ribeye hamburger pork loin sirloin kevin bresaola boudin chuck flank. Ham hock pork belly chicken jerky rump bresaola.

107 |
108 | 109 |
110 |

Section 3

111 |

Shank fatback pastrami short loin, turkey jowl kielbasa ribeye chicken jerky drumstick flank ham. Swine shankle pork belly kielbasa shoulder flank jowl, sirloin doner. Kevin tri-tip bresaola leberkas. Swine ball tip cow strip steak. Ham filet mignon pork chop, pork fatback andouille pork loin shoulder jowl swine strip steak turducken prosciutto rump.

112 | 113 | 114 | 115 |

Tongue tri-tip pastrami, shoulder rump pork belly ground round. Ham hock chuck leberkas doner, strip steak corned beef tri-tip capicola. Rump turkey ham sausage shankle. Flank shankle pork chop ham hock. Shankle venison kielbasa, pancetta swine beef ball tip t-bone bacon hamburger ground round ribeye flank. Turducken bacon bresaola, chicken kevin boudin ball tip strip steak filet mignon pork turkey shank ground round. Kielbasa fatback prosciutto pork chop, jerky ground round leberkas boudin ball tip beef shankle shoulder swine brisket.

116 |
117 | 118 |
119 |

Section 4

120 | 121 | 122 |

To section 1 Shoulder cow tenderloin chuck, pork chop jerky doner leberkas. Chuck sausage hamburger, kevin beef pork chop pork shoulder ground round ball tip turducken flank. Bresaola tri-tip meatloaf, salami venison tail pig shank shankle jowl sausage brisket cow biltong turducken. Swine turducken hamburger ball tip short loin prosciutto kevin jowl tri-tip. Doner meatloaf pork brisket.

123 |
124 |
125 |
126 | or 127 |
128 |
129 | 130 | 131 | 132 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular Scrollspy Demo 7 | 8 | 75 | 76 | 77 |
78 | 86 |

Angular.js Scrollspy Example

87 |
88 |

Section 1

89 |

Bacon ipsum dolor sit amet sausage tail capicola ground round hamburger ham hock. Short ribs pig andouille meatball, pastrami tri-tip fatback ham hock shank kielbasa swine. Rump pancetta jerky kielbasa doner beef ribs tongue hamburger strip steak drumstick andouille shoulder shank flank. Swine drumstick meatball pig beef sausage strip steak.

90 | 91 | 92 |
93 | 94 |
95 |

Section 2

96 |

Bacon strip steak ground round, tongue pastrami short ribs pork chop venison turducken sausage sirloin. Flank chicken pork chop capicola turkey turducken cow pork loin biltong meatball drumstick pancetta filet mignon ground round fatback. Ham hock jerky short ribs brisket. Meatloaf shoulder pork chop capicola, sirloin swine pig pork. Jerky ribeye hamburger pork loin sirloin kevin bresaola boudin chuck flank. Ham hock pork belly chicken jerky rump bresaola.

97 |
98 | 99 |
100 |

Section 3

101 |

Shank fatback pastrami short loin, turkey jowl kielbasa ribeye chicken jerky drumstick flank ham. Swine shankle pork belly kielbasa shoulder flank jowl, sirloin doner. Kevin tri-tip bresaola leberkas. Swine ball tip cow strip steak. Ham filet mignon pork chop, pork fatback andouille pork loin shoulder jowl swine strip steak turducken prosciutto rump.

102 | 103 | 104 | 105 |

Tongue tri-tip pastrami, shoulder rump pork belly ground round. Ham hock chuck leberkas doner, strip steak corned beef tri-tip capicola. Rump turkey ham sausage shankle. Flank shankle pork chop ham hock. Shankle venison kielbasa, pancetta swine beef ball tip t-bone bacon hamburger ground round ribeye flank. Turducken bacon bresaola, chicken kevin boudin ball tip strip steak filet mignon pork turkey shank ground round. Kielbasa fatback prosciutto pork chop, jerky ground round leberkas boudin ball tip beef shankle shoulder swine brisket.

106 |
107 | 108 |
109 |

Section 4

110 | 111 | 112 |

Shoulder cow tenderloin chuck, pork chop jerky doner leberkas. Chuck sausage hamburger, kevin beef pork chop pork shoulder ground round ball tip turducken flank. Bresaola tri-tip meatloaf, salami venison tail pig shank shankle jowl sausage brisket cow biltong turducken. Swine turducken hamburger ball tip short loin prosciutto kevin jowl tri-tip. Doner meatloaf pork brisket.

113 |
114 |
115 | or 116 |
117 |
118 | 119 | 120 | 121 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /example/toc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular Scroll TOC Example 7 | 8 | 84 | 85 | 86 |
87 | 103 |

Angular.js Container Scroll Example

104 |
105 |

Section 1

106 |
107 |

Sub Section 1

108 |

Bacon ipsum dolor sit amet sausage tail capicola ground round hamburger ham hock. Short ribs pig andouille meatball, pastrami tri-tip fatback ham hock shank kielbasa swine. Rump pancetta jerky kielbasa doner beef ribs tongue hamburger strip steak drumstick andouille shoulder shank flank. Swine drumstick meatball pig beef sausage strip steak.

109 |
110 |
111 |

Sub Section 2

112 |

Bacon ipsum dolor sit amet sausage tail capicola ground round hamburger ham hock. Short ribs pig andouille meatball, pastrami tri-tip fatback ham hock shank kielbasa swine. Rump pancetta jerky kielbasa doner beef ribs tongue hamburger strip steak drumstick andouille shoulder shank flank. Swine drumstick meatball pig beef sausage strip steak.

113 |
114 |
115 |

Sub Section 3

116 |

Bacon ipsum dolor sit amet sausage tail capicola ground round hamburger ham hock. Short ribs pig andouille meatball, pastrami tri-tip fatback ham hock shank kielbasa swine. Rump pancetta jerky kielbasa doner beef ribs tongue hamburger strip steak drumstick andouille shoulder shank flank. Swine drumstick meatball pig beef sausage strip steak.

117 |
118 |
119 |

Sub Section 4

120 |

Bacon ipsum dolor sit amet sausage tail capicola ground round hamburger ham hock. Short ribs pig andouille meatball, pastrami tri-tip fatback ham hock shank kielbasa swine. Rump pancetta jerky kielbasa doner beef ribs tongue hamburger strip steak drumstick andouille shoulder shank flank. Swine drumstick meatball pig beef sausage strip steak.

121 |
122 |
123 | 124 |
125 |

Section 2

126 |

Bacon strip steak ground round, tongue pastrami short ribs pork chop venison turducken sausage sirloin. Flank chicken pork chop capicola turkey turducken cow pork loin biltong meatball drumstick pancetta filet mignon ground round fatback. Ham hock jerky short ribs brisket. Meatloaf shoulder pork chop capicola, sirloin swine pig pork. Jerky ribeye hamburger pork loin sirloin kevin bresaola boudin chuck flank. Ham hock pork belly chicken jerky rump bresaola.

127 |
128 | 129 |
130 |

Section 3

131 |

Shank fatback pastrami short loin, turkey jowl kielbasa ribeye chicken jerky drumstick flank ham. Swine shankle pork belly kielbasa shoulder flank jowl, sirloin doner. Kevin tri-tip bresaola leberkas. Swine ball tip cow strip steak. Ham filet mignon pork chop, pork fatback andouille pork loin shoulder jowl swine strip steak turducken prosciutto rump.

132 | 133 |

Tongue tri-tip pastrami, shoulder rump pork belly ground round. Ham hock chuck leberkas doner, strip steak corned beef tri-tip capicola. Rump turkey ham sausage shankle. Flank shankle pork chop ham hock. Shankle venison kielbasa, pancetta swine beef ball tip t-bone bacon hamburger ground round ribeye flank. Turducken bacon bresaola, chicken kevin boudin ball tip strip steak filet mignon pork turkey shank ground round. Kielbasa fatback prosciutto pork chop, jerky ground round leberkas boudin ball tip beef shankle shoulder swine brisket.

134 |
135 | 136 |
137 |

Section 4

138 | 139 |

To section 1 Shoulder cow tenderloin chuck, pork chop jerky doner leberkas. Chuck sausage hamburger, kevin beef pork chop pork shoulder ground round ball tip turducken flank. Bresaola tri-tip meatloaf, salami venison tail pig shank shankle jowl sausage brisket cow biltong turducken. Swine turducken hamburger ball tip short loin prosciutto kevin jowl tri-tip. Doner meatloaf pork brisket.

140 |
141 |
142 |
143 | or 144 |
145 | 146 | 147 | 148 | 149 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var clean = require('gulp-rimraf'); 3 | var jshint = require('gulp-jshint'); 4 | var concat = require('gulp-concat'); 5 | var uglify = require('gulp-uglify'); 6 | var ngmin = require('gulp-ng-annotate'); 7 | var sourcemaps = require('gulp-sourcemaps'); 8 | 9 | var karma = require('karma').server; 10 | var karmaConfPath = './test/karma.conf.js'; 11 | var karmaConf = require(karmaConfPath); 12 | 13 | var sources = [ 14 | 'src/module.js', 15 | 'src/helpers.js', 16 | 'src/services/request-animation.js', 17 | 'src/services/spy-api.js', 18 | 'src/services/scroll-container-api.js', 19 | 'src/directives/smooth-scroll.js', 20 | 'src/directives/spy-context.js', 21 | 'src/directives/scroll-container.js', 22 | 'src/directives/scrollspy.js' 23 | ]; 24 | 25 | var targets = 'angular-scroll.{js,min.js,min.js.map}'; 26 | 27 | gulp.task('clean', function() { 28 | gulp.src(targets) 29 | .pipe(clean()); 30 | }); 31 | 32 | gulp.task('lint', function() { 33 | gulp.src(sources) 34 | .pipe(jshint()) 35 | .pipe(jshint.reporter('default')); 36 | }); 37 | 38 | gulp.task('karma', function(done) { 39 | karma.start({configFile: __dirname + '/test/karma.conf.js', singleRun: true}, done); 40 | }); 41 | 42 | gulp.task('compress', function() { 43 | //Development version 44 | gulp.src(sources) 45 | .pipe(concat('angular-scroll.js', { newLine: '\n\n' })) 46 | .pipe(ngmin()) 47 | .pipe(gulp.dest('./')); 48 | 49 | //Minified version 50 | gulp.src(sources) 51 | .pipe(sourcemaps.init()) 52 | .pipe(concat('angular-scroll.min.js', { newLine: '\n\n' })) 53 | .pipe(ngmin()) 54 | .pipe(uglify()) 55 | .pipe(sourcemaps.write('./')) 56 | .pipe(gulp.dest('./')); 57 | }); 58 | 59 | gulp.task('build', ['clean', 'compress']); 60 | gulp.task('test', ['lint', 'karma']); 61 | gulp.task('default', ['test', 'build']); 62 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('angular'); 2 | require('./angular-scroll'); 3 | 4 | module.exports = 'duScroll'; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-scroll", 3 | "version": "1.0.2", 4 | "description": "Scrollspy, animated scrollTo and scroll events", 5 | "keywords": [ 6 | "angular", 7 | "smooth-scroll", 8 | "scrollspy", 9 | "scrollTo", 10 | "scrolling" 11 | ], 12 | "main": "index.js", 13 | "repository": { 14 | "type": "git", 15 | "url": "git://git@github.com:oblador/angular-scroll.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/oblador/angular-scroll/issues" 19 | }, 20 | "scripts": { 21 | "start": "http-server -p 8888", 22 | "test": "./node_modules/karma/bin/karma start test/karma.conf.js", 23 | "build": "./node_modules/.bin/gulp build", 24 | "test-ci": "./node_modules/.bin/gulp test", 25 | "test-single-run": "./node_modules/karma/bin/karma start test/karma.conf.js --single-run", 26 | "update-webdriver": "node_modules/protractor/bin/webdriver-manager update", 27 | "protractor": "node_modules/protractor/bin/protractor test/protractor.conf.js" 28 | }, 29 | "author": { 30 | "name": "Joel Arvidsson", 31 | "email": "joel@oblador.se" 32 | }, 33 | "license": "MIT", 34 | "devDependencies": { 35 | "gulp": "^3.8.11", 36 | "gulp-concat": "^2.5.1", 37 | "gulp-jshint": "^1.9.2", 38 | "gulp-ng-annotate": "^0.5.2", 39 | "gulp-rimraf": "^0.1.1", 40 | "gulp-sourcemaps": "^1.3.0", 41 | "gulp-uglify": "^1.1.0", 42 | "http-server": "^0.7.4", 43 | "jasmine-given": "^2.6.2", 44 | "karma": "^0.12.31", 45 | "karma-chrome-launcher": "^0.1.7", 46 | "karma-firefox-launcher": "^0.1.4", 47 | "karma-jasmine": "^0.3.5", 48 | "protractor": "^1.7.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/directives/scroll-container.js: -------------------------------------------------------------------------------- 1 | angular.module('duScroll.scrollContainer', ['duScroll.scrollContainerAPI']) 2 | .directive('duScrollContainer', function(scrollContainerAPI){ 3 | 'use strict'; 4 | 5 | return { 6 | restrict: 'A', 7 | scope: true, 8 | compile: function compile(tElement, tAttrs, transclude) { 9 | return { 10 | pre: function preLink($scope, iElement, iAttrs, controller) { 11 | iAttrs.$observe('duScrollContainer', function(element) { 12 | if(angular.isString(element)) { 13 | element = document.getElementById(element); 14 | } 15 | 16 | element = (angular.isElement(element) ? angular.element(element) : iElement); 17 | scrollContainerAPI.setContainer($scope, element); 18 | $scope.$on('$destroy', function() { 19 | scrollContainerAPI.removeContainer($scope); 20 | }); 21 | }); 22 | } 23 | }; 24 | } 25 | }; 26 | }); 27 | -------------------------------------------------------------------------------- /src/directives/scrollspy.js: -------------------------------------------------------------------------------- 1 | angular.module('duScroll.scrollspy', ['duScroll.spyAPI']) 2 | .directive('duScrollspy', function(spyAPI, duScrollOffset, $timeout, $rootScope) { 3 | 'use strict'; 4 | 5 | var Spy = function(targetElementOrId, $scope, $element, offset) { 6 | if(angular.isElement(targetElementOrId)) { 7 | this.target = targetElementOrId; 8 | } else if(angular.isString(targetElementOrId)) { 9 | this.targetId = targetElementOrId; 10 | } 11 | this.$scope = $scope; 12 | this.$element = $element; 13 | this.offset = offset; 14 | }; 15 | 16 | Spy.prototype.getTargetElement = function() { 17 | if (!this.target && this.targetId) { 18 | this.target = document.getElementById(this.targetId) || document.getElementsByName(this.targetId)[0]; 19 | } 20 | return this.target; 21 | }; 22 | 23 | Spy.prototype.getTargetPosition = function() { 24 | var target = this.getTargetElement(); 25 | if(target) { 26 | return target.getBoundingClientRect(); 27 | } 28 | }; 29 | 30 | Spy.prototype.flushTargetCache = function() { 31 | if(this.targetId) { 32 | this.target = undefined; 33 | } 34 | }; 35 | 36 | return { 37 | link: function ($scope, $element, $attr) { 38 | var href = $attr.ngHref || $attr.href; 39 | var targetId; 40 | 41 | if (href && href.indexOf('#') !== -1) { 42 | targetId = href.replace(/.*(?=#[^\s]+$)/, '').substring(1); 43 | } else if($attr.duScrollspy) { 44 | targetId = $attr.duScrollspy; 45 | } else if($attr.duSmoothScroll) { 46 | targetId = $attr.duSmoothScroll; 47 | } 48 | if(!targetId) return; 49 | 50 | // Run this in the next execution loop so that the scroll context has a chance 51 | // to initialize 52 | var timeoutPromise = $timeout(function() { 53 | var spy = new Spy(targetId, $scope, $element, -($attr.offset ? parseInt($attr.offset, 10) : duScrollOffset)); 54 | spyAPI.addSpy(spy); 55 | 56 | $scope.$on('$locationChangeSuccess', spy.flushTargetCache.bind(spy)); 57 | var deregisterOnStateChange = $rootScope.$on('$stateChangeSuccess', spy.flushTargetCache.bind(spy)); 58 | $scope.$on('$destroy', function() { 59 | spyAPI.removeSpy(spy); 60 | deregisterOnStateChange(); 61 | }); 62 | }, 0, false); 63 | $scope.$on('$destroy', function() {$timeout.cancel(timeoutPromise);}); 64 | } 65 | }; 66 | }); 67 | -------------------------------------------------------------------------------- /src/directives/smooth-scroll.js: -------------------------------------------------------------------------------- 1 | angular.module('duScroll.smoothScroll', ['duScroll.scrollHelpers', 'duScroll.scrollContainerAPI']) 2 | .directive('duSmoothScroll', function(duScrollDuration, duScrollOffset, scrollContainerAPI) { 3 | 'use strict'; 4 | 5 | return { 6 | link : function($scope, $element, $attr) { 7 | $element.on('click', function(e) { 8 | if((!$attr.href || $attr.href.indexOf('#') === -1) && $attr.duSmoothScroll === '') return; 9 | 10 | var id = $attr.href ? $attr.href.replace(/.*(?=#[^\s]+$)/, '').substring(1) : $attr.duSmoothScroll; 11 | 12 | var target = document.getElementById(id) || document.getElementsByName(id)[0]; 13 | if(!target || !target.getBoundingClientRect) return; 14 | 15 | if (e.stopPropagation) e.stopPropagation(); 16 | if (e.preventDefault) e.preventDefault(); 17 | 18 | var offset = $attr.offset ? parseInt($attr.offset, 10) : duScrollOffset; 19 | var duration = $attr.duration ? parseInt($attr.duration, 10) : duScrollDuration; 20 | var container = scrollContainerAPI.getContainer($scope); 21 | 22 | container.duScrollToElement( 23 | angular.element(target), 24 | isNaN(offset) ? 0 : offset, 25 | isNaN(duration) ? 0 : duration 26 | ); 27 | }); 28 | } 29 | }; 30 | }); 31 | -------------------------------------------------------------------------------- /src/directives/spy-context.js: -------------------------------------------------------------------------------- 1 | angular.module('duScroll.spyContext', ['duScroll.spyAPI']) 2 | .directive('duSpyContext', function(spyAPI) { 3 | 'use strict'; 4 | 5 | return { 6 | restrict: 'A', 7 | scope: true, 8 | compile: function compile(tElement, tAttrs, transclude) { 9 | return { 10 | pre: function preLink($scope, iElement, iAttrs, controller) { 11 | spyAPI.createContext($scope); 12 | } 13 | }; 14 | } 15 | }; 16 | }); 17 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | angular.module('duScroll.scrollHelpers', ['duScroll.requestAnimation']) 2 | .run(function($window, $q, cancelAnimation, requestAnimation, duScrollEasing, duScrollDuration, duScrollOffset, duScrollCancelOnEvents) { 3 | 'use strict'; 4 | 5 | var proto = {}; 6 | 7 | var isDocument = function(el) { 8 | return (typeof HTMLDocument !== 'undefined' && el instanceof HTMLDocument) || (el.nodeType && el.nodeType === el.DOCUMENT_NODE); 9 | }; 10 | 11 | var isElement = function(el) { 12 | return (typeof HTMLElement !== 'undefined' && el instanceof HTMLElement) || (el.nodeType && el.nodeType === el.ELEMENT_NODE); 13 | }; 14 | 15 | var unwrap = function(el) { 16 | return isElement(el) || isDocument(el) ? el : el[0]; 17 | }; 18 | 19 | proto.duScrollTo = function(left, top, duration, easing) { 20 | var aliasFn; 21 | if(angular.isElement(left)) { 22 | aliasFn = this.duScrollToElement; 23 | } else if(angular.isDefined(duration)) { 24 | aliasFn = this.duScrollToAnimated; 25 | } 26 | if(aliasFn) { 27 | return aliasFn.apply(this, arguments); 28 | } 29 | var el = unwrap(this); 30 | if(isDocument(el)) { 31 | return $window.scrollTo(left, top); 32 | } 33 | el.scrollLeft = left; 34 | el.scrollTop = top; 35 | }; 36 | 37 | var scrollAnimation, deferred; 38 | proto.duScrollToAnimated = function(left, top, duration, easing) { 39 | if(duration && !easing) { 40 | easing = duScrollEasing; 41 | } 42 | var startLeft = this.duScrollLeft(), 43 | startTop = this.duScrollTop(), 44 | deltaLeft = Math.round(left - startLeft), 45 | deltaTop = Math.round(top - startTop); 46 | 47 | var startTime = null, progress = 0; 48 | var el = this; 49 | 50 | var cancelScrollAnimation = function($event) { 51 | if (!$event || (progress && $event.which > 0)) { 52 | if(duScrollCancelOnEvents) { 53 | el.unbind(duScrollCancelOnEvents, cancelScrollAnimation); 54 | } 55 | cancelAnimation(scrollAnimation); 56 | deferred.reject(); 57 | scrollAnimation = null; 58 | } 59 | }; 60 | 61 | if(scrollAnimation) { 62 | cancelScrollAnimation(); 63 | } 64 | deferred = $q.defer(); 65 | 66 | if(duration === 0 || (!deltaLeft && !deltaTop)) { 67 | if(duration === 0) { 68 | el.duScrollTo(left, top); 69 | } 70 | deferred.resolve(); 71 | return deferred.promise; 72 | } 73 | 74 | var animationStep = function(timestamp) { 75 | if (startTime === null) { 76 | startTime = timestamp; 77 | } 78 | 79 | progress = timestamp - startTime; 80 | var percent = (progress >= duration ? 1 : easing(progress/duration)); 81 | 82 | el.scrollTo( 83 | startLeft + Math.ceil(deltaLeft * percent), 84 | startTop + Math.ceil(deltaTop * percent) 85 | ); 86 | if(percent < 1) { 87 | scrollAnimation = requestAnimation(animationStep); 88 | } else { 89 | if(duScrollCancelOnEvents) { 90 | el.unbind(duScrollCancelOnEvents, cancelScrollAnimation); 91 | } 92 | scrollAnimation = null; 93 | deferred.resolve(); 94 | } 95 | }; 96 | 97 | //Fix random mobile safari bug when scrolling to top by hitting status bar 98 | el.duScrollTo(startLeft, startTop); 99 | 100 | if(duScrollCancelOnEvents) { 101 | el.bind(duScrollCancelOnEvents, cancelScrollAnimation); 102 | } 103 | 104 | scrollAnimation = requestAnimation(animationStep); 105 | return deferred.promise; 106 | }; 107 | 108 | proto.duScrollToElement = function(target, offset, duration, easing) { 109 | var el = unwrap(this); 110 | if(!angular.isNumber(offset) || isNaN(offset)) { 111 | offset = duScrollOffset; 112 | } 113 | var top = this.duScrollTop() + unwrap(target).getBoundingClientRect().top - offset; 114 | if(isElement(el)) { 115 | top -= el.getBoundingClientRect().top; 116 | } 117 | return this.duScrollTo(0, top, duration, easing); 118 | }; 119 | 120 | proto.duScrollLeft = function(value, duration, easing) { 121 | if(angular.isNumber(value)) { 122 | return this.duScrollTo(value, this.duScrollTop(), duration, easing); 123 | } 124 | var el = unwrap(this); 125 | if(isDocument(el)) { 126 | return $window.scrollX || document.documentElement.scrollLeft || document.body.scrollLeft; 127 | } 128 | return el.scrollLeft; 129 | }; 130 | proto.duScrollTop = function(value, duration, easing) { 131 | if(angular.isNumber(value)) { 132 | return this.duScrollTo(this.duScrollLeft(), value, duration, easing); 133 | } 134 | var el = unwrap(this); 135 | if(isDocument(el)) { 136 | return $window.scrollY || document.documentElement.scrollTop || document.body.scrollTop; 137 | } 138 | return el.scrollTop; 139 | }; 140 | 141 | proto.duScrollToElementAnimated = function(target, offset, duration, easing) { 142 | return this.duScrollToElement(target, offset, duration || duScrollDuration, easing); 143 | }; 144 | 145 | proto.duScrollTopAnimated = function(top, duration, easing) { 146 | return this.duScrollTop(top, duration || duScrollDuration, easing); 147 | }; 148 | 149 | proto.duScrollLeftAnimated = function(left, duration, easing) { 150 | return this.duScrollLeft(left, duration || duScrollDuration, easing); 151 | }; 152 | 153 | angular.forEach(proto, function(fn, key) { 154 | angular.element.prototype[key] = fn; 155 | 156 | //Remove prefix if not already claimed by jQuery / ui.utils 157 | var unprefixed = key.replace(/^duScroll/, 'scroll'); 158 | if(angular.isUndefined(angular.element.prototype[unprefixed])) { 159 | angular.element.prototype[unprefixed] = fn; 160 | } 161 | }); 162 | 163 | }); 164 | -------------------------------------------------------------------------------- /src/module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * x is a value between 0 and 1, indicating where in the animation you are. 3 | */ 4 | var duScrollDefaultEasing = function (x) { 5 | 'use strict'; 6 | 7 | if(x < 0.5) { 8 | return Math.pow(x*2, 2)/2; 9 | } 10 | return 1-Math.pow((1-x)*2, 2)/2; 11 | }; 12 | 13 | var duScroll = angular.module('duScroll', [ 14 | 'duScroll.scrollspy', 15 | 'duScroll.smoothScroll', 16 | 'duScroll.scrollContainer', 17 | 'duScroll.spyContext', 18 | 'duScroll.scrollHelpers' 19 | ]) 20 | //Default animation duration for smoothScroll directive 21 | .value('duScrollDuration', 350) 22 | //Scrollspy debounce interval, set to 0 to disable 23 | .value('duScrollSpyWait', 100) 24 | //Scrollspy forced refresh interval, use if your content changes or reflows without scrolling. 25 | //0 to disable 26 | .value('duScrollSpyRefreshInterval', 0) 27 | //Wether or not multiple scrollspies can be active at once 28 | .value('duScrollGreedy', false) 29 | //Default offset for smoothScroll directive 30 | .value('duScrollOffset', 0) 31 | //Default easing function for scroll animation 32 | .value('duScrollEasing', duScrollDefaultEasing) 33 | //Which events on the container (such as body) should cancel scroll animations 34 | .value('duScrollCancelOnEvents', 'scroll mousedown mousewheel touchmove keydown') 35 | //Whether or not to activate the last scrollspy, when page/container bottom is reached 36 | .value('duScrollBottomSpy', false) 37 | //Active class name 38 | .value('duScrollActiveClass', 'active'); 39 | 40 | if (typeof module !== 'undefined' && module && module.exports) { 41 | module.exports = duScroll; 42 | } 43 | -------------------------------------------------------------------------------- /src/services/request-animation.js: -------------------------------------------------------------------------------- 1 | //Adapted from https://gist.github.com/paulirish/1579671 2 | angular.module('duScroll.polyfill', []) 3 | .factory('polyfill', function($window) { 4 | 'use strict'; 5 | 6 | var vendors = ['webkit', 'moz', 'o', 'ms']; 7 | 8 | return function(fnName, fallback) { 9 | if($window[fnName]) { 10 | return $window[fnName]; 11 | } 12 | var suffix = fnName.substr(0, 1).toUpperCase() + fnName.substr(1); 13 | for(var key, i = 0; i < vendors.length; i++) { 14 | key = vendors[i]+suffix; 15 | if($window[key]) { 16 | return $window[key]; 17 | } 18 | } 19 | return fallback; 20 | }; 21 | }); 22 | 23 | angular.module('duScroll.requestAnimation', ['duScroll.polyfill']) 24 | .factory('requestAnimation', function(polyfill, $timeout) { 25 | 'use strict'; 26 | 27 | var lastTime = 0; 28 | var fallback = function(callback, element) { 29 | var currTime = new Date().getTime(); 30 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 31 | var id = $timeout(function() { callback(currTime + timeToCall); }, 32 | timeToCall); 33 | lastTime = currTime + timeToCall; 34 | return id; 35 | }; 36 | 37 | return polyfill('requestAnimationFrame', fallback); 38 | }) 39 | .factory('cancelAnimation', function(polyfill, $timeout) { 40 | 'use strict'; 41 | 42 | var fallback = function(promise) { 43 | $timeout.cancel(promise); 44 | }; 45 | 46 | return polyfill('cancelAnimationFrame', fallback); 47 | }); 48 | -------------------------------------------------------------------------------- /src/services/scroll-container-api.js: -------------------------------------------------------------------------------- 1 | angular.module('duScroll.scrollContainerAPI', []) 2 | .factory('scrollContainerAPI', function($document) { 3 | 'use strict'; 4 | 5 | var containers = {}; 6 | 7 | var setContainer = function(scope, element) { 8 | var id = scope.$id; 9 | containers[id] = element; 10 | return id; 11 | }; 12 | 13 | var getContainerId = function(scope) { 14 | if(containers[scope.$id]) { 15 | return scope.$id; 16 | } 17 | if(scope.$parent) { 18 | return getContainerId(scope.$parent); 19 | } 20 | return; 21 | }; 22 | 23 | var getContainer = function(scope) { 24 | var id = getContainerId(scope); 25 | return id ? containers[id] : $document; 26 | }; 27 | 28 | var removeContainer = function(scope) { 29 | var id = getContainerId(scope); 30 | if(id) { 31 | delete containers[id]; 32 | } 33 | }; 34 | 35 | return { 36 | getContainerId: getContainerId, 37 | getContainer: getContainer, 38 | setContainer: setContainer, 39 | removeContainer: removeContainer 40 | }; 41 | }); 42 | -------------------------------------------------------------------------------- /src/services/spy-api.js: -------------------------------------------------------------------------------- 1 | angular.module('duScroll.spyAPI', ['duScroll.scrollContainerAPI']) 2 | .factory('spyAPI', function($rootScope, $timeout, $interval, $window, $document, scrollContainerAPI, duScrollGreedy, duScrollSpyWait, duScrollSpyRefreshInterval, duScrollBottomSpy, duScrollActiveClass) { 3 | 'use strict'; 4 | 5 | var createScrollHandler = function(context) { 6 | var timer = false, queued = false; 7 | var handler = function() { 8 | queued = false; 9 | var container = context.container, 10 | containerEl = container[0], 11 | containerOffset = 0, 12 | bottomReached; 13 | 14 | if (typeof HTMLElement !== 'undefined' && containerEl instanceof HTMLElement || containerEl.nodeType && containerEl.nodeType === containerEl.ELEMENT_NODE) { 15 | containerOffset = containerEl.getBoundingClientRect().top; 16 | bottomReached = Math.round(containerEl.scrollTop + containerEl.clientHeight) >= containerEl.scrollHeight; 17 | } else { 18 | var documentScrollHeight = $document[0].body.scrollHeight || $document[0].documentElement.scrollHeight; // documentElement for IE11 19 | bottomReached = Math.round($window.pageYOffset + $window.innerHeight) >= documentScrollHeight; 20 | } 21 | var compareProperty = (duScrollBottomSpy && bottomReached ? 'bottom' : 'top'); 22 | 23 | var i, currentlyActive, toBeActive, spies, spy, pos; 24 | spies = context.spies; 25 | currentlyActive = context.currentlyActive; 26 | toBeActive = undefined; 27 | 28 | for(i = 0; i < spies.length; i++) { 29 | spy = spies[i]; 30 | pos = spy.getTargetPosition(); 31 | if (!pos || !spy.$element) continue; 32 | 33 | if((duScrollBottomSpy && bottomReached) || (pos.top + spy.offset - containerOffset < 20 && (duScrollGreedy || pos.top*-1 + containerOffset) < pos.height)) { 34 | //Find the one closest the viewport top or the page bottom if it's reached 35 | if(!toBeActive || toBeActive[compareProperty] < pos[compareProperty]) { 36 | toBeActive = { 37 | spy: spy 38 | }; 39 | toBeActive[compareProperty] = pos[compareProperty]; 40 | } 41 | } 42 | } 43 | 44 | if(toBeActive) { 45 | toBeActive = toBeActive.spy; 46 | } 47 | if(currentlyActive === toBeActive || (duScrollGreedy && !toBeActive)) return; 48 | if(currentlyActive && currentlyActive.$element) { 49 | currentlyActive.$element.removeClass(duScrollActiveClass); 50 | $rootScope.$broadcast( 51 | 'duScrollspy:becameInactive', 52 | currentlyActive.$element, 53 | angular.element(currentlyActive.getTargetElement()) 54 | ); 55 | } 56 | if(toBeActive) { 57 | toBeActive.$element.addClass(duScrollActiveClass); 58 | $rootScope.$broadcast( 59 | 'duScrollspy:becameActive', 60 | toBeActive.$element, 61 | angular.element(toBeActive.getTargetElement()) 62 | ); 63 | } 64 | context.currentlyActive = toBeActive; 65 | }; 66 | 67 | if(!duScrollSpyWait) { 68 | return handler; 69 | } 70 | 71 | //Debounce for potential performance savings 72 | return function() { 73 | if(!timer) { 74 | handler(); 75 | timer = $timeout(function() { 76 | timer = false; 77 | if(queued) { 78 | handler(); 79 | } 80 | }, duScrollSpyWait, false); 81 | } else { 82 | queued = true; 83 | } 84 | }; 85 | }; 86 | 87 | var contexts = {}; 88 | 89 | var createContext = function($scope) { 90 | var id = $scope.$id; 91 | var context = { 92 | spies: [] 93 | }; 94 | 95 | context.handler = createScrollHandler(context); 96 | contexts[id] = context; 97 | 98 | $scope.$on('$destroy', function() { 99 | destroyContext($scope); 100 | }); 101 | 102 | return id; 103 | }; 104 | 105 | var destroyContext = function($scope) { 106 | var id = $scope.$id; 107 | var context = contexts[id], container = context.container; 108 | if(context.intervalPromise) { 109 | $interval.cancel(context.intervalPromise); 110 | } 111 | if(container) { 112 | container.off('scroll', context.handler); 113 | } 114 | delete contexts[id]; 115 | }; 116 | 117 | var defaultContextId = createContext($rootScope); 118 | 119 | var getContextForScope = function(scope) { 120 | if(contexts[scope.$id]) { 121 | return contexts[scope.$id]; 122 | } 123 | if(scope.$parent) { 124 | return getContextForScope(scope.$parent); 125 | } 126 | return contexts[defaultContextId]; 127 | }; 128 | 129 | var getContextForSpy = function(spy) { 130 | var context, contextId, scope = spy.$scope; 131 | if(scope) { 132 | return getContextForScope(scope); 133 | } 134 | //No scope, most likely destroyed 135 | for(contextId in contexts) { 136 | context = contexts[contextId]; 137 | if(context.spies.indexOf(spy) !== -1) { 138 | return context; 139 | } 140 | } 141 | }; 142 | 143 | var isElementInDocument = function(element) { 144 | while (element.parentNode) { 145 | element = element.parentNode; 146 | if (element === document) { 147 | return true; 148 | } 149 | } 150 | return false; 151 | }; 152 | 153 | var addSpy = function(spy) { 154 | var context = getContextForSpy(spy); 155 | if (!context) return; 156 | context.spies.push(spy); 157 | if (!context.container || !isElementInDocument(context.container)) { 158 | if(context.container) { 159 | context.container.off('scroll', context.handler); 160 | } 161 | context.container = scrollContainerAPI.getContainer(spy.$scope); 162 | if (duScrollSpyRefreshInterval && !context.intervalPromise) { 163 | context.intervalPromise = $interval(context.handler, duScrollSpyRefreshInterval, 0, false); 164 | } 165 | context.container.on('scroll', context.handler).triggerHandler('scroll'); 166 | } 167 | }; 168 | 169 | var removeSpy = function(spy) { 170 | var context = getContextForSpy(spy); 171 | if(spy === context.currentlyActive) { 172 | $rootScope.$broadcast('duScrollspy:becameInactive', context.currentlyActive.$element); 173 | context.currentlyActive = null; 174 | } 175 | var i = context.spies.indexOf(spy); 176 | if(i !== -1) { 177 | context.spies.splice(i, 1); 178 | } 179 | spy.$element = null; 180 | }; 181 | 182 | return { 183 | addSpy: addSpy, 184 | removeSpy: removeSpy, 185 | createContext: createContext, 186 | destroyContext: destroyContext, 187 | getContextForScope: getContextForScope 188 | }; 189 | }); 190 | -------------------------------------------------------------------------------- /test/e2e/pages/container-page.js: -------------------------------------------------------------------------------- 1 | var ContainerPage = (function () { 2 | var ptor = protractor.getInstance(); 3 | 4 | function ContainerPage() { 5 | this.links = element.all(By.css('nav a')); 6 | this.activeLinks = element.all(By.css('nav a.active')); 7 | this.topButton = element(By.css('footer button:first-child')); 8 | } 9 | 10 | ContainerPage.prototype.visitPage = function () { 11 | browser.get('/example/container.html'); 12 | }; 13 | 14 | ContainerPage.prototype.scrollToTop = function () { 15 | return this.topButton.click().then(function() { 16 | ptor.sleep(5000); 17 | }); 18 | }; 19 | 20 | ContainerPage.prototype.scrollToSection = function (nr) { 21 | return this.links.get(nr).click().then(function() { 22 | ptor.sleep(500); 23 | }); 24 | }; 25 | 26 | ContainerPage.prototype.scrollBackAndForth = function (nr) { 27 | var self = this; 28 | return self.links.get(nr).click().then(function() { 29 | ptor.sleep(100); 30 | return self.topButton.click().then(function() { 31 | ptor.sleep(100); 32 | return self.links.get(nr).click().then(function() { 33 | ptor.sleep(500); 34 | }); 35 | }); 36 | }); 37 | }; 38 | 39 | return ContainerPage; 40 | 41 | })(); 42 | 43 | module.exports = ContainerPage; 44 | -------------------------------------------------------------------------------- /test/e2e/pages/default-page.js: -------------------------------------------------------------------------------- 1 | var DefaultPage = (function () { 2 | var ptor = protractor.getInstance(); 3 | 4 | function DefaultPage() { 5 | this.links = element.all(By.css('nav a')); 6 | this.activeLinks = element.all(By.css('nav a.active')); 7 | this.topButton = element(By.css('footer button:first-child')); 8 | } 9 | 10 | DefaultPage.prototype.visitPage = function () { 11 | browser.get('/example/index.html'); 12 | }; 13 | 14 | DefaultPage.prototype.scrollToTop = function () { 15 | return this.topButton.click().then(function() { 16 | ptor.sleep(5000); 17 | }); 18 | }; 19 | 20 | DefaultPage.prototype.scrollToSection = function (nr) { 21 | return this.links.get(nr).click().then(function() { 22 | ptor.sleep(500); 23 | }); 24 | }; 25 | 26 | DefaultPage.prototype.scrollBackAndForth = function (nr) { 27 | var self = this; 28 | return self.links.get(nr).click().then(function() { 29 | ptor.sleep(100); 30 | return self.topButton.click().then(function() { 31 | ptor.sleep(100); 32 | return self.links.get(nr).click().then(function() { 33 | ptor.sleep(500); 34 | }); 35 | }); 36 | }); 37 | }; 38 | 39 | return DefaultPage; 40 | 41 | })(); 42 | 43 | module.exports = DefaultPage; 44 | -------------------------------------------------------------------------------- /test/e2e/scenarios.js: -------------------------------------------------------------------------------- 1 | require('jasmine-given'); 2 | 3 | var pages = { 4 | DefaultPage: require('./pages/default-page'), 5 | ContainerPage: require('./pages/container-page') 6 | }; 7 | 8 | describe('duScroll', function() { 9 | for(var pageName in pages) { 10 | describe(pageName, function() { 11 | var page = new pages[pageName](); 12 | page.visitPage(); 13 | 14 | describe('duScrollspy', function() { 15 | 16 | beforeEach(function() { 17 | page.visitPage(); 18 | }); 19 | 20 | it('should have a section 1 link', function() { 21 | expect(page.links.count()).toBe(4); 22 | expect(page.links.first().getText()).toMatch(/Section 1/); 23 | }); 24 | 25 | it('should make the section 1 link active after clicking it', function() { 26 | page.scrollToSection(0).then(function() { 27 | expect(page.activeLinks.count()).toBe(1); 28 | expect(page.links.get(0).getAttribute('class')).toBe('active'); 29 | }); 30 | }); 31 | 32 | if(pageName === 'DefaultPage') { 33 | //Omit this test for the container page since the first link is at the top 34 | it('should make all links inactive after scrolling to top', function() { 35 | page.scrollToTop().then(function() { 36 | expect(page.activeLinks.count()).toBe(0); 37 | }); 38 | }); 39 | it('should support elements with name instead of id attribute', function() { 40 | page.scrollToSection(1).then(function() { 41 | expect(page.activeLinks.count()).toBe(1); 42 | expect(page.links.get(1).getAttribute('class')).toBe('active'); 43 | }); 44 | }); 45 | it('should support du-smooth-scroll attribute instead of href', function() { 46 | page.scrollToSection(2).then(function() { 47 | expect(page.activeLinks.count()).toBe(1); 48 | expect(page.links.get(2).getAttribute('class')).toBe('active'); 49 | }); 50 | }); 51 | } 52 | 53 | it('should cancel all animations except the last one', function() { 54 | page.scrollBackAndForth(2).then(function() { 55 | expect(page.activeLinks.count()).toBe(1); 56 | expect(page.links.get(2).getAttribute('class')).toBe('active'); 57 | }); 58 | }); 59 | }); 60 | }); 61 | } 62 | }); 63 | 64 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var testFiles = [ 4 | 'bower_components/angular/angular.js', 5 | 'bower_components/angular-mocks/angular-mocks.js', 6 | 'src/**/*.js', 7 | 'test/unit/**/*.js' 8 | ]; 9 | 10 | module.exports = function(config){ 11 | var options = { 12 | basePath : path.dirname(__dirname), 13 | files : testFiles, 14 | autoWatch : true, 15 | frameworks: ['jasmine'], 16 | browsers : ['Firefox', 'Chrome'], 17 | customLaunchers: { 18 | Chrome_travis_ci: { 19 | base: 'Chrome', 20 | flags: ['--no-sandbox'] 21 | } 22 | }, 23 | plugins : [ 24 | 'karma-chrome-launcher', 25 | 'karma-firefox-launcher', 26 | 'karma-jasmine' 27 | ], 28 | junitReporter : { 29 | outputFile: 'test_out/unit.xml', 30 | suite: 'unit' 31 | } 32 | }; 33 | if(process.env.TRAVIS){ 34 | options.browsers = ['Chrome_travis_ci']; 35 | } 36 | config.set(options); 37 | }; 38 | 39 | module.exports.testFiles = testFiles; 40 | -------------------------------------------------------------------------------- /test/protractor.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | allScriptsTimeout: 11000, 3 | 4 | specs: [ 5 | 'e2e/*.js' 6 | ], 7 | 8 | capabilities: { 9 | 'browserName': 'chrome' 10 | }, 11 | 12 | baseUrl: 'http://localhost:8888/', 13 | 14 | framework: 'jasmine', 15 | 16 | jasmineNodeOpts: { 17 | defaultTimeoutInterval: 30000 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /test/unit/defaultsSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('defaults', function() { 4 | beforeEach(module('duScroll')); 5 | 6 | it('should should have a default scroll duration', inject(function(duScrollDuration) { 7 | expect(duScrollDuration).not.toBe(null); 8 | })); 9 | 10 | it('should should have a default easing function', inject(function(duScrollEasing) { 11 | expect(duScrollEasing).not.toBe(null); 12 | })); 13 | 14 | it('should have a default active class', inject(function(duScrollActiveClass) { 15 | expect(duScrollActiveClass).toEqual('active'); 16 | })); 17 | }); 18 | -------------------------------------------------------------------------------- /test/unit/helpersSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('jqlite helpers', function() { 4 | beforeEach(module('duScroll')); 5 | 6 | describe('scrollTopAnimated', function() { 7 | var duration = 100; 8 | 9 | it('should return a promise', inject(function($document, $q, $rootScope) { 10 | var deferred = $q.defer(); 11 | var promise = $document.scrollTopAnimated(100, duration); 12 | expect(promise).toEqual(jasmine.any(Object)); 13 | expect(Object.keys(promise)).toEqual(Object.keys(deferred.promise)); 14 | 15 | it('which should resolve when done animating', function(done){ 16 | spyOn(promise, 'then'); 17 | $rootScope.$digest(); 18 | setTimeout(function() { 19 | expect(promise.then).toHaveBeenCalled(); 20 | done(); 21 | }, duration*1.5); 22 | }); 23 | })); 24 | 25 | it('should cancel previous animation', function(done){inject(function($document, $rootScope) { 26 | var rejected = false; 27 | $document.scrollTopAnimated(200, duration) 28 | .catch(function() { 29 | rejected = true; 30 | }) 31 | .finally(function() { 32 | expect(rejected).toEqual(true); 33 | done(); 34 | }); 35 | $document.scrollTopAnimated(300, duration); 36 | $rootScope.$digest(); 37 | })}); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/unit/servicesSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('service', function() { 4 | beforeEach(module('duScroll')); 5 | 6 | 7 | describe('requestAnimation', function() { 8 | it('should contain an requestAnimation service', inject(function(requestAnimation) { 9 | expect(requestAnimation).not.toBe(null); 10 | })); 11 | 12 | describe('callback', function() { 13 | var timerCallback; 14 | beforeEach(function() { 15 | timerCallback = jasmine.createSpy("timerCallback"); 16 | }); 17 | 18 | it('should be called within 100ms', function(done){inject(['requestAnimation',function(requestAnimation) { 19 | requestAnimation(timerCallback); 20 | expect(timerCallback).not.toHaveBeenCalled(); 21 | setTimeout(function() { 22 | expect(timerCallback).toHaveBeenCalled(); 23 | done(); 24 | }, 100); 25 | }])}); 26 | }); 27 | }); 28 | 29 | 30 | describe('scrollContainerAPI', function() { 31 | it('should contain an scrollContainerAPI service', inject(function(scrollContainerAPI) { 32 | expect(scrollContainerAPI).not.toBe(null); 33 | })); 34 | 35 | it('should default to $document', inject(['$rootScope', 'scrollContainerAPI', '$document' ,function($rootScope, containers, $document) { 36 | expect(containers.getContainer($rootScope)).toBe($document); 37 | }])); 38 | }); 39 | 40 | 41 | describe('spyAPI', function() { 42 | it('should contain an spyAPI service', inject(function(spyAPI) { 43 | expect(spyAPI).not.toBe(null); 44 | })); 45 | 46 | it('should return $id when creating a context', inject(['$rootScope', 'spyAPI' ,function($rootScope, spyAPI) { 47 | expect(spyAPI.createContext($rootScope)).toBe($rootScope.$id); 48 | }])); 49 | 50 | it('should return a root context object by default', inject(['$rootScope', 'spyAPI' ,function($rootScope, spyAPI) { 51 | var testScope = $rootScope.$new(); 52 | expect(spyAPI.getContextForScope(testScope)).toBe(spyAPI.getContextForScope($rootScope)); 53 | }])); 54 | 55 | it('should not return default context if specified', inject(['$rootScope', 'spyAPI' ,function($rootScope, spyAPI) { 56 | var testScope = $rootScope.$new(); 57 | spyAPI.createContext(testScope); 58 | expect(spyAPI.getContextForScope(testScope)).not.toBe(spyAPI.getContextForScope($rootScope)); 59 | }])); 60 | 61 | it('should return parent scope context', inject(['$rootScope', 'spyAPI' ,function($rootScope, spyAPI) { 62 | var testScope = $rootScope.$new(); 63 | var childScope = testScope.$new(); 64 | expect(spyAPI.getContextForScope(childScope)).toBe(spyAPI.getContextForScope(testScope)); 65 | }])); 66 | 67 | it('should return default context when destroyed', inject(['$rootScope', 'spyAPI' ,function($rootScope, spyAPI) { 68 | var testScope = $rootScope.$new(); 69 | spyAPI.createContext(testScope); 70 | 71 | expect(spyAPI.getContextForScope(testScope)).not.toBe(spyAPI.getContextForScope($rootScope)); 72 | 73 | spyAPI.destroyContext(testScope); 74 | 75 | expect(spyAPI.getContextForScope(testScope)).toBe(spyAPI.getContextForScope($rootScope)); 76 | }])); 77 | }); 78 | }); 79 | --------------------------------------------------------------------------------