UNPKG

19.8 kBJavaScriptView Raw
1/**
2 * x is a value between 0 and 1, indicating where in the animation you are.
3 */
4var 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
13var 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
40if (typeof module !== 'undefined' && module && module.exports) {
41 module.exports = duScroll;
42}
43
44
45angular.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
211angular.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
232angular.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
259angular.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
450angular.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
493angular.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
525angular.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
543angular.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
571angular.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}]);