590 lines
17 KiB
JavaScript
590 lines
17 KiB
JavaScript
/**
|
|
* 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 = angular.element.prototype;
|
|
|
|
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.scrollTo = function(left, top, duration, easing) {
|
|
var aliasFn;
|
|
if(angular.isElement(left)) {
|
|
aliasFn = this.scrollToElement;
|
|
} else if(duration) {
|
|
aliasFn = this.scrollToAnimated;
|
|
}
|
|
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.scrollToAnimated = function(left, top, duration, easing) {
|
|
if(duration && !easing) {
|
|
easing = duScrollEasing;
|
|
}
|
|
var startLeft = this.scrollLeft(),
|
|
startTop = this.scrollTop(),
|
|
deltaLeft = Math.round(left - startLeft),
|
|
deltaTop = Math.round(top - startTop);
|
|
|
|
var startTime = null;
|
|
var el = this;
|
|
|
|
var cancelOnEvents = 'scroll mousedown mousewheel touchmove keydown';
|
|
var cancelScrollAnimation = function($event) {
|
|
if (!$event || $event.which > 0) {
|
|
el.unbind(cancelOnEvents, cancelScrollAnimation);
|
|
cancelAnimation(scrollAnimation);
|
|
deferred.reject();
|
|
scrollAnimation = null;
|
|
}
|
|
};
|
|
|
|
if(scrollAnimation) {
|
|
cancelScrollAnimation();
|
|
}
|
|
deferred = $q.defer();
|
|
|
|
if(!deltaLeft && !deltaTop) {
|
|
deferred.resolve();
|
|
return deferred.promise;
|
|
}
|
|
|
|
var animationStep = function(timestamp) {
|
|
if (startTime === null) {
|
|
startTime = timestamp;
|
|
}
|
|
|
|
var 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.scrollTo(startLeft, startTop);
|
|
|
|
el.bind(cancelOnEvents, cancelScrollAnimation);
|
|
|
|
scrollAnimation = requestAnimation(animationStep);
|
|
return deferred.promise;
|
|
};
|
|
|
|
proto.scrollToElement = function(target, offset, duration, easing) {
|
|
var el = unwrap(this);
|
|
if(!angular.isNumber(offset) || isNaN(offset)) {
|
|
offset = duScrollOffset;
|
|
}
|
|
var top = this.scrollTop() + unwrap(target).getBoundingClientRect().top - offset;
|
|
if(isElement(el)) {
|
|
top -= el.getBoundingClientRect().top;
|
|
}
|
|
return this.scrollTo(0, top, duration, easing);
|
|
};
|
|
|
|
var overloaders = {
|
|
scrollLeft: function(value, duration, easing) {
|
|
if(angular.isNumber(value)) {
|
|
return this.scrollTo(value, this.scrollTop(), duration, easing);
|
|
}
|
|
var el = unwrap(this);
|
|
if(isDocument(el)) {
|
|
return $window.scrollX || document.documentElement.scrollLeft || document.body.scrollLeft;
|
|
}
|
|
return el.scrollLeft;
|
|
},
|
|
scrollTop: function(value, duration, easing) {
|
|
if(angular.isNumber(value)) {
|
|
return this.scrollTo(this.scrollTop(), value, duration, easing);
|
|
}
|
|
var el = unwrap(this);
|
|
if(isDocument(el)) {
|
|
return $window.scrollY || document.documentElement.scrollTop || document.body.scrollTop;
|
|
}
|
|
return el.scrollTop;
|
|
}
|
|
};
|
|
|
|
proto.scrollToElementAnimated = function(target, offset, duration, easing) {
|
|
return this.scrollToElement(target, offset, duration || duScrollDuration, easing);
|
|
};
|
|
|
|
proto.scrollTopAnimated = function(top, duration, easing) {
|
|
return this.scrollTop(top, duration || duScrollDuration, easing);
|
|
};
|
|
|
|
proto.scrollLeftAnimated = function(left, duration, easing) {
|
|
return this.scrollLeft(left, duration || duScrollDuration, easing);
|
|
};
|
|
|
|
//Add duration and easing functionality to existing jQuery getter/setters
|
|
var overloadScrollPos = function(superFn, overloadFn) {
|
|
return function(value, duration, easing) {
|
|
if(duration) {
|
|
return overloadFn.apply(this, arguments);
|
|
}
|
|
return superFn.apply(this, arguments);
|
|
};
|
|
};
|
|
|
|
for(var methodName in overloaders) {
|
|
proto[methodName] = (proto[methodName] ? overloadScrollPos(proto[methodName], overloaders[methodName]) : overloaders[methodName]);
|
|
}
|
|
}]);
|
|
|
|
|
|
//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", "scrollContainerAPI", "duScrollGreedy", "duScrollSpyWait", function($rootScope, $timeout, 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;
|
|
|
|
if (typeof HTMLElement !== 'undefined' && containerEl instanceof HTMLElement || containerEl.nodeType && containerEl.nodeType === containerEl.ELEMENT_NODE) {
|
|
containerOffset = containerEl.getBoundingClientRect().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(pos.top + spy.offset - containerOffset < 20 && (pos.top*-1 + containerOffset) < pos.height) {
|
|
if(!toBeActive || toBeActive.top < pos.top) {
|
|
toBeActive = {
|
|
top: pos.top,
|
|
spy: spy
|
|
};
|
|
}
|
|
}
|
|
}
|
|
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.$element.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.$element.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);
|
|
}
|
|
};
|
|
|
|
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.scrollToElement(
|
|
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, $element, offset) {
|
|
if(angular.isElement(targetElementOrId)) {
|
|
this.target = targetElementOrId;
|
|
} else if(angular.isString(targetElementOrId)) {
|
|
this.targetId = targetElementOrId;
|
|
}
|
|
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, $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);
|
|
}
|
|
};
|
|
}]);
|