123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597 |
- /**
- * x is a value between 0 and 1, indicating where in the animation you are.
- */
- var duScrollDefaultEasing = function (x) {
- 'use strict';
- if(x < 0.5) {
- return Math.pow(x*2, 2)/2;
- }
- return 1-Math.pow((1-x)*2, 2)/2;
- };
- angular.module('duScroll', [
- 'duScroll.scrollspy',
- 'duScroll.smoothScroll',
- 'duScroll.scrollContainer',
- 'duScroll.spyContext',
- 'duScroll.scrollHelpers'
- ])
- //Default animation duration for smoothScroll directive
- .value('duScrollDuration', 350)
- //Scrollspy debounce interval, set to 0 to disable
- .value('duScrollSpyWait', 100)
- //Wether or not multiple scrollspies can be active at once
- .value('duScrollGreedy', false)
- //Default offset for smoothScroll directive
- .value('duScrollOffset', 0)
- //Default easing function for scroll animation
- .value('duScrollEasing', duScrollDefaultEasing);
- angular.module('duScroll.scrollHelpers', ['duScroll.requestAnimation'])
- .run(["$window", "$q", "cancelAnimation", "requestAnimation", "duScrollEasing", "duScrollDuration", "duScrollOffset", function($window, $q, cancelAnimation, requestAnimation, duScrollEasing, duScrollDuration, duScrollOffset) {
- 'use strict';
- var proto = {};
- var isDocument = function(el) {
- return (typeof HTMLDocument !== 'undefined' && el instanceof HTMLDocument) || (el.nodeType && el.nodeType === el.DOCUMENT_NODE);
- };
- var isElement = function(el) {
- return (typeof HTMLElement !== 'undefined' && el instanceof HTMLElement) || (el.nodeType && el.nodeType === el.ELEMENT_NODE);
- };
- var unwrap = function(el) {
- return isElement(el) || isDocument(el) ? el : el[0];
- };
- proto.duScrollTo = function(left, top, duration, easing) {
- var aliasFn;
- if(angular.isElement(left)) {
- aliasFn = this.duScrollToElement;
- } else if(angular.isDefined(duration)) {
- aliasFn = this.duScrollToAnimated;
- }
- if(aliasFn) {
- return aliasFn.apply(this, arguments);
- }
- var el = unwrap(this);
- if(isDocument(el)) {
- return $window.scrollTo(left, top);
- }
- el.scrollLeft = left;
- el.scrollTop = top;
- };
- var scrollAnimation, deferred;
- proto.duScrollToAnimated = function(left, top, duration, easing) {
- if(duration && !easing) {
- easing = duScrollEasing;
- }
- var startLeft = this.duScrollLeft(),
- startTop = this.duScrollTop(),
- deltaLeft = Math.round(left - startLeft),
- deltaTop = Math.round(top - startTop);
- var startTime = null, progress = 0;
- var el = this;
- var cancelOnEvents = 'scroll mousedown mousewheel touchmove keydown';
- var cancelScrollAnimation = function($event) {
- if (!$event || (progress && $event.which > 0)) {
- el.unbind(cancelOnEvents, cancelScrollAnimation);
- cancelAnimation(scrollAnimation);
- deferred.reject();
- scrollAnimation = null;
- }
- };
- if(scrollAnimation) {
- cancelScrollAnimation();
- }
- deferred = $q.defer();
- if(duration === 0 || (!deltaLeft && !deltaTop)) {
- if(duration === 0) {
- el.duScrollTo(left, top);
- }
- deferred.resolve();
- return deferred.promise;
- }
- var animationStep = function(timestamp) {
- if (startTime === null) {
- startTime = timestamp;
- }
- progress = timestamp - startTime;
- var percent = (progress >= duration ? 1 : easing(progress/duration));
- el.scrollTo(
- startLeft + Math.ceil(deltaLeft * percent),
- startTop + Math.ceil(deltaTop * percent)
- );
- if(percent < 1) {
- scrollAnimation = requestAnimation(animationStep);
- } else {
- el.unbind(cancelOnEvents, cancelScrollAnimation);
- scrollAnimation = null;
- deferred.resolve();
- }
- };
- //Fix random mobile safari bug when scrolling to top by hitting status bar
- el.duScrollTo(startLeft, startTop);
- el.bind(cancelOnEvents, cancelScrollAnimation);
- scrollAnimation = requestAnimation(animationStep);
- return deferred.promise;
- };
- proto.duScrollToElement = function(target, offset, duration, easing) {
- var el = unwrap(this);
- if(!angular.isNumber(offset) || isNaN(offset)) {
- offset = duScrollOffset;
- }
- var top = this.duScrollTop() + unwrap(target).getBoundingClientRect().top - offset;
- if(isElement(el)) {
- top -= el.getBoundingClientRect().top;
- }
- return this.duScrollTo(0, top, duration, easing);
- };
- proto.duScrollLeft = function(value, duration, easing) {
- if(angular.isNumber(value)) {
- return this.duScrollTo(value, this.duScrollTop(), duration, easing);
- }
- var el = unwrap(this);
- if(isDocument(el)) {
- return $window.scrollX || document.documentElement.scrollLeft || document.body.scrollLeft;
- }
- return el.scrollLeft;
- };
- proto.duScrollTop = function(value, duration, easing) {
- if(angular.isNumber(value)) {
- return this.duScrollTo(this.duScrollLeft(), value, duration, easing);
- }
- var el = unwrap(this);
- if(isDocument(el)) {
- return $window.scrollY || document.documentElement.scrollTop || document.body.scrollTop;
- }
- return el.scrollTop;
- };
- proto.duScrollToElementAnimated = function(target, offset, duration, easing) {
- return this.duScrollToElement(target, offset, duration || duScrollDuration, easing);
- };
- proto.duScrollTopAnimated = function(top, duration, easing) {
- return this.duScrollTop(top, duration || duScrollDuration, easing);
- };
- proto.duScrollLeftAnimated = function(left, duration, easing) {
- return this.duScrollLeft(left, duration || duScrollDuration, easing);
- };
- angular.forEach(proto, function(fn, key) {
- angular.element.prototype[key] = fn;
- //Remove prefix if not already claimed by jQuery / ui.utils
- var unprefixed = key.replace(/^duScroll/, 'scroll');
- if(angular.isUndefined(angular.element.prototype[unprefixed])) {
- angular.element.prototype[unprefixed] = fn;
- }
- });
- }]);
- //Adapted from https://gist.github.com/paulirish/1579671
- angular.module('duScroll.polyfill', [])
- .factory('polyfill', ["$window", function($window) {
- 'use strict';
- var vendors = ['webkit', 'moz', 'o', 'ms'];
- return function(fnName, fallback) {
- if($window[fnName]) {
- return $window[fnName];
- }
- var suffix = fnName.substr(0, 1).toUpperCase() + fnName.substr(1);
- for(var key, i = 0; i < vendors.length; i++) {
- key = vendors[i]+suffix;
- if($window[key]) {
- return $window[key];
- }
- }
- return fallback;
- };
- }]);
- angular.module('duScroll.requestAnimation', ['duScroll.polyfill'])
- .factory('requestAnimation', ["polyfill", "$timeout", function(polyfill, $timeout) {
- 'use strict';
- var lastTime = 0;
- var fallback = function(callback, element) {
- var currTime = new Date().getTime();
- var timeToCall = Math.max(0, 16 - (currTime - lastTime));
- var id = $timeout(function() { callback(currTime + timeToCall); },
- timeToCall);
- lastTime = currTime + timeToCall;
- return id;
- };
- return polyfill('requestAnimationFrame', fallback);
- }])
- .factory('cancelAnimation', ["polyfill", "$timeout", function(polyfill, $timeout) {
- 'use strict';
- var fallback = function(promise) {
- $timeout.cancel(promise);
- };
- return polyfill('cancelAnimationFrame', fallback);
- }]);
- angular.module('duScroll.spyAPI', ['duScroll.scrollContainerAPI'])
- .factory('spyAPI', ["$rootScope", "$timeout", "$window", "$document", "scrollContainerAPI", "duScrollGreedy", "duScrollSpyWait", function($rootScope, $timeout, $window, $document, scrollContainerAPI, duScrollGreedy, duScrollSpyWait) {
- 'use strict';
- var createScrollHandler = function(context) {
- var timer = false, queued = false;
- var handler = function() {
- queued = false;
- var container = context.container,
- containerEl = container[0],
- containerOffset = 0,
- bottomReached;
- if (typeof HTMLElement !== 'undefined' && containerEl instanceof HTMLElement || containerEl.nodeType && containerEl.nodeType === containerEl.ELEMENT_NODE) {
- containerOffset = containerEl.getBoundingClientRect().top;
- bottomReached = Math.round(containerEl.scrollTop + containerEl.clientHeight) >= containerEl.scrollHeight;
- } else {
- bottomReached = Math.round($window.pageYOffset + $window.innerHeight) >= $document[0].body.scrollHeight;
- }
- var compareProperty = (bottomReached ? 'bottom' : 'top');
- var i, currentlyActive, toBeActive, spies, spy, pos;
- spies = context.spies;
- currentlyActive = context.currentlyActive;
- toBeActive = undefined;
- for(i = 0; i < spies.length; i++) {
- spy = spies[i];
- pos = spy.getTargetPosition();
- if (!pos) continue;
- if(bottomReached || (pos.top + spy.offset - containerOffset < 20 && (duScrollGreedy || pos.top*-1 + containerOffset) < pos.height)) {
- //Find the one closest the viewport top or the page bottom if it's reached
- if(!toBeActive || toBeActive[compareProperty] < pos[compareProperty]) {
- toBeActive = {
- spy: spy
- };
- toBeActive[compareProperty] = pos[compareProperty];
- }
- }
- }
- if(toBeActive) {
- toBeActive = toBeActive.spy;
- }
- if(currentlyActive === toBeActive || (duScrollGreedy && !toBeActive)) return;
- if(currentlyActive) {
- currentlyActive.$element.removeClass('active');
- $rootScope.$broadcast('duScrollspy:becameInactive', currentlyActive.$element);
- }
- if(toBeActive) {
- toBeActive.$element.addClass('active');
- $rootScope.$broadcast('duScrollspy:becameActive', toBeActive.$element);
- }
- context.currentlyActive = toBeActive;
- };
- if(!duScrollSpyWait) {
- return handler;
- }
- //Debounce for potential performance savings
- return function() {
- if(!timer) {
- handler();
- timer = $timeout(function() {
- timer = false;
- if(queued) {
- handler();
- }
- }, duScrollSpyWait, false);
- } else {
- queued = true;
- }
- };
- };
- var contexts = {};
- var createContext = function($scope) {
- var id = $scope.$id;
- var context = {
- spies: []
- };
- context.handler = createScrollHandler(context);
- contexts[id] = context;
- $scope.$on('$destroy', function() {
- destroyContext($scope);
- });
- return id;
- };
- var destroyContext = function($scope) {
- var id = $scope.$id;
- var context = contexts[id], container = context.container;
- if(container) {
- container.off('scroll', context.handler);
- }
- delete contexts[id];
- };
- var defaultContextId = createContext($rootScope);
- var getContextForScope = function(scope) {
- if(contexts[scope.$id]) {
- return contexts[scope.$id];
- }
- if(scope.$parent) {
- return getContextForScope(scope.$parent);
- }
- return contexts[defaultContextId];
- };
- var getContextForSpy = function(spy) {
- var context, contextId, scope = spy.$scope;
- if(scope) {
- return getContextForScope(scope);
- }
- //No scope, most likely destroyed
- for(contextId in contexts) {
- context = contexts[contextId];
- if(context.spies.indexOf(spy) !== -1) {
- return context;
- }
- }
- };
- var isElementInDocument = function(element) {
- while (element.parentNode) {
- element = element.parentNode;
- if (element === document) {
- return true;
- }
- }
- return false;
- };
- var addSpy = function(spy) {
- var context = getContextForSpy(spy);
- if (!context) return;
- context.spies.push(spy);
- if (!context.container || !isElementInDocument(context.container)) {
- if(context.container) {
- context.container.off('scroll', context.handler);
- }
- context.container = scrollContainerAPI.getContainer(spy.$scope);
- context.container.on('scroll', context.handler).triggerHandler('scroll');
- }
- };
- var removeSpy = function(spy) {
- var context = getContextForSpy(spy);
- if(spy === context.currentlyActive) {
- context.currentlyActive = null;
- }
- var i = context.spies.indexOf(spy);
- if(i !== -1) {
- context.spies.splice(i, 1);
- }
- spy.$element = null;
- };
- return {
- addSpy: addSpy,
- removeSpy: removeSpy,
- createContext: createContext,
- destroyContext: destroyContext,
- getContextForScope: getContextForScope
- };
- }]);
- angular.module('duScroll.scrollContainerAPI', [])
- .factory('scrollContainerAPI', ["$document", function($document) {
- 'use strict';
- var containers = {};
- var setContainer = function(scope, element) {
- var id = scope.$id;
- containers[id] = element;
- return id;
- };
- var getContainerId = function(scope) {
- if(containers[scope.$id]) {
- return scope.$id;
- }
- if(scope.$parent) {
- return getContainerId(scope.$parent);
- }
- return;
- };
- var getContainer = function(scope) {
- var id = getContainerId(scope);
- return id ? containers[id] : $document;
- };
- var removeContainer = function(scope) {
- var id = getContainerId(scope);
- if(id) {
- delete containers[id];
- }
- };
- return {
- getContainerId: getContainerId,
- getContainer: getContainer,
- setContainer: setContainer,
- removeContainer: removeContainer
- };
- }]);
- angular.module('duScroll.smoothScroll', ['duScroll.scrollHelpers', 'duScroll.scrollContainerAPI'])
- .directive('duSmoothScroll', ["duScrollDuration", "duScrollOffset", "scrollContainerAPI", function(duScrollDuration, duScrollOffset, scrollContainerAPI) {
- 'use strict';
- return {
- link : function($scope, $element, $attr) {
- $element.on('click', function(e) {
- if(!$attr.href || $attr.href.indexOf('#') === -1) return;
- var target = document.getElementById($attr.href.replace(/.*(?=#[^\s]+$)/, '').substring(1));
- if(!target || !target.getBoundingClientRect) return;
- if (e.stopPropagation) e.stopPropagation();
- if (e.preventDefault) e.preventDefault();
- var offset = $attr.offset ? parseInt($attr.offset, 10) : duScrollOffset;
- var duration = $attr.duration ? parseInt($attr.duration, 10) : duScrollDuration;
- var container = scrollContainerAPI.getContainer($scope);
- container.duScrollToElement(
- angular.element(target),
- isNaN(offset) ? 0 : offset,
- isNaN(duration) ? 0 : duration
- );
- });
- }
- };
- }]);
- angular.module('duScroll.spyContext', ['duScroll.spyAPI'])
- .directive('duSpyContext', ["spyAPI", function(spyAPI) {
- 'use strict';
- return {
- restrict: 'A',
- scope: true,
- compile: function compile(tElement, tAttrs, transclude) {
- return {
- pre: function preLink($scope, iElement, iAttrs, controller) {
- spyAPI.createContext($scope);
- }
- };
- }
- };
- }]);
- angular.module('duScroll.scrollContainer', ['duScroll.scrollContainerAPI'])
- .directive('duScrollContainer', ["scrollContainerAPI", function(scrollContainerAPI){
- 'use strict';
- return {
- restrict: 'A',
- scope: true,
- compile: function compile(tElement, tAttrs, transclude) {
- return {
- pre: function preLink($scope, iElement, iAttrs, controller) {
- iAttrs.$observe('duScrollContainer', function(element) {
- if(angular.isString(element)) {
- element = document.getElementById(element);
- }
- element = (angular.isElement(element) ? angular.element(element) : iElement);
- scrollContainerAPI.setContainer($scope, element);
- $scope.$on('$destroy', function() {
- scrollContainerAPI.removeContainer($scope);
- });
- });
- }
- };
- }
- };
- }]);
- angular.module('duScroll.scrollspy', ['duScroll.spyAPI'])
- .directive('duScrollspy', ["spyAPI", "duScrollOffset", "$timeout", "$rootScope", function(spyAPI, duScrollOffset, $timeout, $rootScope) {
- 'use strict';
- var Spy = function(targetElementOrId, $scope, $element, offset) {
- if(angular.isElement(targetElementOrId)) {
- this.target = targetElementOrId;
- } else if(angular.isString(targetElementOrId)) {
- this.targetId = targetElementOrId;
- }
- this.$scope = $scope;
- this.$element = $element;
- this.offset = offset;
- };
- Spy.prototype.getTargetElement = function() {
- if (!this.target && this.targetId) {
- this.target = document.getElementById(this.targetId);
- }
- return this.target;
- };
- Spy.prototype.getTargetPosition = function() {
- var target = this.getTargetElement();
- if(target) {
- return target.getBoundingClientRect();
- }
- };
- Spy.prototype.flushTargetCache = function() {
- if(this.targetId) {
- this.target = undefined;
- }
- };
- return {
- link: function ($scope, $element, $attr) {
- var href = $attr.ngHref || $attr.href;
- var targetId;
- if (href && href.indexOf('#') !== -1) {
- targetId = href.replace(/.*(?=#[^\s]+$)/, '').substring(1);
- } else if($attr.duScrollspy) {
- targetId = $attr.duScrollspy;
- }
- if(!targetId) return;
- // Run this in the next execution loop so that the scroll context has a chance
- // to initialize
- $timeout(function() {
- var spy = new Spy(targetId, $scope, $element, -($attr.offset ? parseInt($attr.offset, 10) : duScrollOffset));
- spyAPI.addSpy(spy);
- $scope.$on('$destroy', function() {
- spyAPI.removeSpy(spy);
- });
- $scope.$on('$locationChangeSuccess', spy.flushTargetCache.bind(spy));
- $rootScope.$on('$stateChangeSuccess', spy.flushTargetCache.bind(spy));
- }, 0, false);
- }
- };
- }]);
|