UNPKG

17.7 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
13angular.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 //Wether or not multiple scrollspies can be active at once
25 .value('duScrollGreedy', false)
26 //Default offset for smoothScroll directive
27 .value('duScrollOffset', 0)
28 //Default easing function for scroll animation
29 .value('duScrollEasing', duScrollDefaultEasing);
30
31
32angular.module('duScroll.scrollHelpers', ['duScroll.requestAnimation'])
33.run(["$window", "$q", "cancelAnimation", "requestAnimation", "duScrollEasing", "duScrollDuration", "duScrollOffset", function($window, $q, cancelAnimation, requestAnimation, duScrollEasing, duScrollDuration, duScrollOffset) {
34 'use strict';
35
36 var proto = {};
37
38 var isDocument = function(el) {
39 return (typeof HTMLDocument !== 'undefined' && el instanceof HTMLDocument) || (el.nodeType && el.nodeType === el.DOCUMENT_NODE);
40 };
41
42 var isElement = function(el) {
43 return (typeof HTMLElement !== 'undefined' && el instanceof HTMLElement) || (el.nodeType && el.nodeType === el.ELEMENT_NODE);
44 };
45
46 var unwrap = function(el) {
47 return isElement(el) || isDocument(el) ? el : el[0];
48 };
49
50 proto.duScrollTo = function(left, top, duration, easing) {
51 var aliasFn;
52 if(angular.isElement(left)) {
53 aliasFn = this.duScrollToElement;
54 } else if(angular.isDefined(duration)) {
55 aliasFn = this.duScrollToAnimated;
56 }
57 if(aliasFn) {
58 return aliasFn.apply(this, arguments);
59 }
60 var el = unwrap(this);
61 if(isDocument(el)) {
62 return $window.scrollTo(left, top);
63 }
64 el.scrollLeft = left;
65 el.scrollTop = top;
66 };
67
68 var scrollAnimation, deferred;
69 proto.duScrollToAnimated = function(left, top, duration, easing) {
70 if(duration && !easing) {
71 easing = duScrollEasing;
72 }
73 var startLeft = this.duScrollLeft(),
74 startTop = this.duScrollTop(),
75 deltaLeft = Math.round(left - startLeft),
76 deltaTop = Math.round(top - startTop);
77
78 var startTime = null;
79 var el = this;
80
81 var cancelOnEvents = 'scroll mousedown mousewheel touchmove keydown';
82 var cancelScrollAnimation = function($event) {
83 if (!$event || $event.which > 0) {
84 el.unbind(cancelOnEvents, cancelScrollAnimation);
85 cancelAnimation(scrollAnimation);
86 deferred.reject();
87 scrollAnimation = null;
88 }
89 };
90
91 if(scrollAnimation) {
92 cancelScrollAnimation();
93 }
94 deferred = $q.defer();
95
96 if(duration === 0 || (!deltaLeft && !deltaTop)) {
97 if(duration === 0) {
98 el.duScrollTo(left, top);
99 }
100 deferred.resolve();
101 return deferred.promise;
102 }
103
104 var animationStep = function(timestamp) {
105 if (startTime === null) {
106 startTime = timestamp;
107 }
108
109 var progress = timestamp - startTime;
110 var percent = (progress >= duration ? 1 : easing(progress/duration));
111
112 el.scrollTo(
113 startLeft + Math.ceil(deltaLeft * percent),
114 startTop + Math.ceil(deltaTop * percent)
115 );
116 if(percent < 1) {
117 scrollAnimation = requestAnimation(animationStep);
118 } else {
119 el.unbind(cancelOnEvents, cancelScrollAnimation);
120 scrollAnimation = null;
121 deferred.resolve();
122 }
123 };
124
125 //Fix random mobile safari bug when scrolling to top by hitting status bar
126 el.duScrollTo(startLeft, startTop);
127
128 el.bind(cancelOnEvents, cancelScrollAnimation);
129
130 scrollAnimation = requestAnimation(animationStep);
131 return deferred.promise;
132 };
133
134 proto.duScrollToElement = function(target, offset, duration, easing) {
135 var el = unwrap(this);
136 if(!angular.isNumber(offset) || isNaN(offset)) {
137 offset = duScrollOffset;
138 }
139 var top = this.duScrollTop() + unwrap(target).getBoundingClientRect().top - offset;
140 if(isElement(el)) {
141 top -= el.getBoundingClientRect().top;
142 }
143 return this.duScrollTo(0, top, duration, easing);
144 };
145
146 proto.duScrollLeft = function(value, duration, easing) {
147 if(angular.isNumber(value)) {
148 return this.duScrollTo(value, this.duScrollTop(), duration, easing);
149 }
150 var el = unwrap(this);
151 if(isDocument(el)) {
152 return $window.scrollX || document.documentElement.scrollLeft || document.body.scrollLeft;
153 }
154 return el.scrollLeft;
155 };
156 proto.duScrollTop = function(value, duration, easing) {
157 if(angular.isNumber(value)) {
158 return this.duScrollTo(this.duScrollLeft(), value, duration, easing);
159 }
160 var el = unwrap(this);
161 if(isDocument(el)) {
162 return $window.scrollY || document.documentElement.scrollTop || document.body.scrollTop;
163 }
164 return el.scrollTop;
165 };
166
167 proto.duScrollToElementAnimated = function(target, offset, duration, easing) {
168 return this.duScrollToElement(target, offset, duration || duScrollDuration, easing);
169 };
170
171 proto.duScrollTopAnimated = function(top, duration, easing) {
172 return this.duScrollTop(top, duration || duScrollDuration, easing);
173 };
174
175 proto.duScrollLeftAnimated = function(left, duration, easing) {
176 return this.duScrollLeft(left, duration || duScrollDuration, easing);
177 };
178
179 angular.forEach(proto, function(fn, key) {
180 angular.element.prototype[key] = fn;
181
182 //Remove prefix if not already claimed by jQuery / ui.utils
183 var unprefixed = key.replace(/^duScroll/, 'scroll');
184 if(angular.isUndefined(angular.element.prototype[unprefixed])) {
185 angular.element.prototype[unprefixed] = fn;
186 }
187 });
188
189}]);
190
191
192//Adapted from https://gist.github.com/paulirish/1579671
193angular.module('duScroll.polyfill', [])
194.factory('polyfill', ["$window", function($window) {
195 'use strict';
196
197 var vendors = ['webkit', 'moz', 'o', 'ms'];
198
199 return function(fnName, fallback) {
200 if($window[fnName]) {
201 return $window[fnName];
202 }
203 var suffix = fnName.substr(0, 1).toUpperCase() + fnName.substr(1);
204 for(var key, i = 0; i < vendors.length; i++) {
205 key = vendors[i]+suffix;
206 if($window[key]) {
207 return $window[key];
208 }
209 }
210 return fallback;
211 };
212}]);
213
214angular.module('duScroll.requestAnimation', ['duScroll.polyfill'])
215.factory('requestAnimation', ["polyfill", "$timeout", function(polyfill, $timeout) {
216 'use strict';
217
218 var lastTime = 0;
219 var fallback = function(callback, element) {
220 var currTime = new Date().getTime();
221 var timeToCall = Math.max(0, 16 - (currTime - lastTime));
222 var id = $timeout(function() { callback(currTime + timeToCall); },
223 timeToCall);
224 lastTime = currTime + timeToCall;
225 return id;
226 };
227
228 return polyfill('requestAnimationFrame', fallback);
229}])
230.factory('cancelAnimation', ["polyfill", "$timeout", function(polyfill, $timeout) {
231 'use strict';
232
233 var fallback = function(promise) {
234 $timeout.cancel(promise);
235 };
236
237 return polyfill('cancelAnimationFrame', fallback);
238}]);
239
240
241angular.module('duScroll.spyAPI', ['duScroll.scrollContainerAPI'])
242.factory('spyAPI', ["$rootScope", "$timeout", "$window", "$document", "scrollContainerAPI", "duScrollGreedy", "duScrollSpyWait", function($rootScope, $timeout, $window, $document, scrollContainerAPI, duScrollGreedy, duScrollSpyWait) {
243 'use strict';
244
245 var createScrollHandler = function(context) {
246 var timer = false, queued = false;
247 var handler = function() {
248 queued = false;
249 var container = context.container,
250 containerEl = container[0],
251 containerOffset = 0,
252 bottomReached;
253
254 if (typeof HTMLElement !== 'undefined' && containerEl instanceof HTMLElement || containerEl.nodeType && containerEl.nodeType === containerEl.ELEMENT_NODE) {
255 containerOffset = containerEl.getBoundingClientRect().top;
256 bottomReached = Math.round(containerEl.scrollTop + containerEl.clientHeight) >= containerEl.scrollHeight;
257 } else {
258 bottomReached = Math.round($window.scrollY + $window.innerHeight) >= $document[0].body.scrollHeight;
259 }
260 var compareProperty = (bottomReached ? 'bottom' : 'top')
261
262 var i, currentlyActive, toBeActive, spies, spy, pos;
263 spies = context.spies;
264 currentlyActive = context.currentlyActive;
265 toBeActive = undefined;
266
267 for(i = 0; i < spies.length; i++) {
268 spy = spies[i];
269 pos = spy.getTargetPosition();
270 if (!pos) continue;
271
272 if(bottomReached || (pos.top + spy.offset - containerOffset < 20 && (duScrollGreedy || pos.top*-1 + containerOffset) < pos.height)) {
273 //Find the one closest the viewport top or the page bottom if it's reached
274 if(!toBeActive || toBeActive[compareProperty] < pos[compareProperty]) {
275 toBeActive = {
276 spy: spy
277 };
278 toBeActive[compareProperty] = pos[compareProperty];
279 }
280 }
281 }
282 if(toBeActive) {
283 toBeActive = toBeActive.spy;
284 }
285 if(currentlyActive === toBeActive || (duScrollGreedy && !toBeActive)) return;
286 if(currentlyActive) {
287 currentlyActive.$element.removeClass('active');
288 $rootScope.$broadcast('duScrollspy:becameInactive', currentlyActive.$element);
289 }
290 if(toBeActive) {
291 toBeActive.$element.addClass('active');
292 $rootScope.$broadcast('duScrollspy:becameActive', toBeActive.$element);
293 }
294 context.currentlyActive = toBeActive;
295 };
296
297 if(!duScrollSpyWait) {
298 return handler;
299 }
300
301 //Debounce for potential performance savings
302 return function() {
303 if(!timer) {
304 handler();
305 timer = $timeout(function() {
306 timer = false;
307 if(queued) {
308 handler();
309 }
310 }, duScrollSpyWait, false);
311 } else {
312 queued = true;
313 }
314 };
315 };
316
317 var contexts = {};
318
319 var createContext = function($scope) {
320 var id = $scope.$id;
321 var context = {
322 spies: []
323 };
324
325 context.handler = createScrollHandler(context);
326 contexts[id] = context;
327
328 $scope.$on('$destroy', function() {
329 destroyContext($scope);
330 });
331
332 return id;
333 };
334
335 var destroyContext = function($scope) {
336 var id = $scope.$id;
337 var context = contexts[id], container = context.container;
338 if(container) {
339 container.off('scroll', context.handler);
340 }
341 delete contexts[id];
342 };
343
344 var defaultContextId = createContext($rootScope);
345
346 var getContextForScope = function(scope) {
347 if(contexts[scope.$id]) {
348 return contexts[scope.$id];
349 }
350 if(scope.$parent) {
351 return getContextForScope(scope.$parent);
352 }
353 return contexts[defaultContextId];
354 };
355
356 var getContextForSpy = function(spy) {
357 var context, contextId, scope = spy.$scope;
358 if(scope) {
359 return getContextForScope(scope);
360 }
361 //No scope, most likely destroyed
362 for(contextId in contexts) {
363 context = contexts[contextId];
364 if(context.spies.indexOf(spy) !== -1) {
365 return context;
366 }
367 }
368 };
369
370 var isElementInDocument = function(element) {
371 while (element.parentNode) {
372 element = element.parentNode;
373 if (element === document) {
374 return true;
375 }
376 }
377 return false;
378 };
379
380 var addSpy = function(spy) {
381 var context = getContextForSpy(spy);
382 if (!context) return;
383 context.spies.push(spy);
384 if (!context.container || !isElementInDocument(context.container)) {
385 if(context.container) {
386 context.container.off('scroll', context.handler);
387 }
388 context.container = scrollContainerAPI.getContainer(spy.$scope);
389 context.container.on('scroll', context.handler).triggerHandler('scroll');
390 }
391 };
392
393 var removeSpy = function(spy) {
394 var context = getContextForSpy(spy);
395 if(spy === context.currentlyActive) {
396 context.currentlyActive = null;
397 }
398 var i = context.spies.indexOf(spy);
399 if(i !== -1) {
400 context.spies.splice(i, 1);
401 }
402 spy.$element = null;
403 };
404
405 return {
406 addSpy: addSpy,
407 removeSpy: removeSpy,
408 createContext: createContext,
409 destroyContext: destroyContext,
410 getContextForScope: getContextForScope
411 };
412}]);
413
414
415angular.module('duScroll.scrollContainerAPI', [])
416.factory('scrollContainerAPI', ["$document", function($document) {
417 'use strict';
418
419 var containers = {};
420
421 var setContainer = function(scope, element) {
422 var id = scope.$id;
423 containers[id] = element;
424 return id;
425 };
426
427 var getContainerId = function(scope) {
428 if(containers[scope.$id]) {
429 return scope.$id;
430 }
431 if(scope.$parent) {
432 return getContainerId(scope.$parent);
433 }
434 return;
435 };
436
437 var getContainer = function(scope) {
438 var id = getContainerId(scope);
439 return id ? containers[id] : $document;
440 };
441
442 var removeContainer = function(scope) {
443 var id = getContainerId(scope);
444 if(id) {
445 delete containers[id];
446 }
447 };
448
449 return {
450 getContainerId: getContainerId,
451 getContainer: getContainer,
452 setContainer: setContainer,
453 removeContainer: removeContainer
454 };
455}]);
456
457
458angular.module('duScroll.smoothScroll', ['duScroll.scrollHelpers', 'duScroll.scrollContainerAPI'])
459.directive('duSmoothScroll', ["duScrollDuration", "duScrollOffset", "scrollContainerAPI", function(duScrollDuration, duScrollOffset, scrollContainerAPI) {
460 'use strict';
461
462 return {
463 link : function($scope, $element, $attr) {
464 $element.on('click', function(e) {
465 if(!$attr.href || $attr.href.indexOf('#') === -1) return;
466
467 var target = document.getElementById($attr.href.replace(/.*(?=#[^\s]+$)/, '').substring(1));
468 if(!target || !target.getBoundingClientRect) return;
469
470 if (e.stopPropagation) e.stopPropagation();
471 if (e.preventDefault) e.preventDefault();
472
473 var offset = $attr.offset ? parseInt($attr.offset, 10) : duScrollOffset;
474 var duration = $attr.duration ? parseInt($attr.duration, 10) : duScrollDuration;
475 var container = scrollContainerAPI.getContainer($scope);
476
477 container.duScrollToElement(
478 angular.element(target),
479 isNaN(offset) ? 0 : offset,
480 isNaN(duration) ? 0 : duration
481 );
482 });
483 }
484 };
485}]);
486
487
488angular.module('duScroll.spyContext', ['duScroll.spyAPI'])
489.directive('duSpyContext', ["spyAPI", function(spyAPI) {
490 'use strict';
491
492 return {
493 restrict: 'A',
494 scope: true,
495 compile: function compile(tElement, tAttrs, transclude) {
496 return {
497 pre: function preLink($scope, iElement, iAttrs, controller) {
498 spyAPI.createContext($scope);
499 }
500 };
501 }
502 };
503}]);
504
505
506angular.module('duScroll.scrollContainer', ['duScroll.scrollContainerAPI'])
507.directive('duScrollContainer', ["scrollContainerAPI", function(scrollContainerAPI){
508 'use strict';
509
510 return {
511 restrict: 'A',
512 scope: true,
513 compile: function compile(tElement, tAttrs, transclude) {
514 return {
515 pre: function preLink($scope, iElement, iAttrs, controller) {
516 iAttrs.$observe('duScrollContainer', function(element) {
517 if(angular.isString(element)) {
518 element = document.getElementById(element);
519 }
520
521 element = (angular.isElement(element) ? angular.element(element) : iElement);
522 scrollContainerAPI.setContainer($scope, element);
523 $scope.$on('$destroy', function() {
524 scrollContainerAPI.removeContainer($scope);
525 });
526 });
527 }
528 };
529 }
530 };
531}]);
532
533
534angular.module('duScroll.scrollspy', ['duScroll.spyAPI'])
535.directive('duScrollspy', ["spyAPI", "duScrollOffset", "$timeout", "$rootScope", function(spyAPI, duScrollOffset, $timeout, $rootScope) {
536 'use strict';
537
538 var Spy = function(targetElementOrId, $scope, $element, offset) {
539 if(angular.isElement(targetElementOrId)) {
540 this.target = targetElementOrId;
541 } else if(angular.isString(targetElementOrId)) {
542 this.targetId = targetElementOrId;
543 }
544 this.$scope = $scope;
545 this.$element = $element;
546 this.offset = offset;
547 };
548
549 Spy.prototype.getTargetElement = function() {
550 if (!this.target && this.targetId) {
551 this.target = document.getElementById(this.targetId);
552 }
553 return this.target;
554 };
555
556 Spy.prototype.getTargetPosition = function() {
557 var target = this.getTargetElement();
558 if(target) {
559 return target.getBoundingClientRect();
560 }
561 };
562
563 Spy.prototype.flushTargetCache = function() {
564 if(this.targetId) {
565 this.target = undefined;
566 }
567 };
568
569 return {
570 link: function ($scope, $element, $attr) {
571 var href = $attr.ngHref || $attr.href;
572 var targetId;
573
574 if (href && href.indexOf('#') !== -1) {
575 targetId = href.replace(/.*(?=#[^\s]+$)/, '').substring(1);
576 } else if($attr.duScrollspy) {
577 targetId = $attr.duScrollspy;
578 }
579 if(!targetId) return;
580
581 // Run this in the next execution loop so that the scroll context has a chance
582 // to initialize
583 $timeout(function() {
584 var spy = new Spy(targetId, $scope, $element, -($attr.offset ? parseInt($attr.offset, 10) : duScrollOffset));
585 spyAPI.addSpy(spy);
586
587 $scope.$on('$destroy', function() {
588 spyAPI.removeSpy(spy);
589 });
590 $scope.$on('$locationChangeSuccess', spy.flushTargetCache.bind(spy));
591 $rootScope.$on('$stateChangeSuccess', spy.flushTargetCache.bind(spy));
592 }, 0, false);
593 }
594 };
595}]);