Created
May 21, 2014 05:53
-
-
Save kkurni/98e664941ac1c373a3ff to your computer and use it in GitHub Desktop.
angular-scroll with duration which work with accordion mobile scroll
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* customised from angular-scroll lib, to support scrolling to moving element... */ | |
/** | |
* x is a value between 0 and 1, indicating where in the animation you are. | |
*/ | |
var duScrollDefaultEasing = function (x) { | |
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.requestAnimation', | |
'duScroll.smoothScroll', | |
'duScroll.scrollContainer', | |
'duScroll.scrollHelpers' | |
]).value('duScrollDuration', 350).value('duScrollEasing', duScrollDefaultEasing); | |
angular.module('duScroll.scrollHelpers', []).run([ | |
'$window', | |
'$q', | |
'cancelAnimation', | |
'requestAnimation', | |
'duScrollEasing', | |
function ($window, $q, cancelAnimation, requestAnimation, duScrollEasing) { | |
var proto = angular.element.prototype; | |
this.$get = function () { | |
return 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.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); | |
if (!deltaLeft && !deltaTop) | |
return; | |
var startTime = null; | |
if (scrollAnimation) { | |
cancelAnimation(scrollAnimation); | |
deferred.reject(); | |
} | |
var el = this; | |
deferred = $q.defer(); | |
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 { | |
scrollAnimation = null; | |
deferred.resolve(); | |
} | |
}; | |
scrollAnimation = requestAnimation(animationStep); | |
return deferred.promise; | |
}; | |
proto.scrollToElement = function (target, offset, duration, easing) { | |
var el = unwrap(this); | |
var top = this.scrollTop() + unwrap(target).getBoundingClientRect().top - offset; | |
if (isElement(el)) { | |
top -= el.getBoundingClientRect().top; | |
} | |
return this.scrollTo(0, top, duration, easing); | |
}; | |
proto.scrollToElementAnimated = function (target, offset, duration, easing) { | |
if (duration && !easing) { | |
easing = duScrollEasing; | |
} | |
var el = this; | |
var top = el.scrollTop() + unwrap(target).getBoundingClientRect().top - offset; | |
if (isElement(el)) { | |
top -= el.getBoundingClientRect().top; | |
} | |
var left = 0; | |
var startLeft = el.scrollLeft(), | |
startTop = el.scrollTop(), | |
deltaLeft = Math.round(left - startLeft), | |
deltaTop = Math.round(top - startTop); | |
if (!deltaLeft && !deltaTop) | |
return; | |
var startTime = null; | |
if (scrollAnimation) { | |
cancelAnimation(scrollAnimation); | |
deferred.reject(); | |
} | |
deferred = $q.defer(); | |
var animationStep = function (timestamp) { | |
if (startTime === null) { | |
startTime = timestamp; | |
} | |
var top = el.scrollTop() + unwrap(target).getBoundingClientRect().top - offset; | |
if (isElement(el)) { | |
top -= el.getBoundingClientRect().top; | |
} | |
startLeft = el.scrollLeft(); | |
startTop = el.scrollTop(); | |
deltaLeft = Math.round(left - startLeft); | |
deltaTop = Math.round(top - startTop); | |
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 { | |
scrollAnimation = null; | |
deferred.resolve(); | |
} | |
}; | |
scrollAnimation = requestAnimation(animationStep); | |
return deferred.promise; | |
}; | |
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; | |
} | |
}; | |
//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) { | |
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) { | |
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) { | |
var fallback = function (promise) { | |
$timeout.cancel(promise); | |
}; | |
return polyfill('cancelAnimationFrame', fallback); | |
} | |
]); | |
angular.module('duScroll.spyAPI', ['duScroll.scrollContainerAPI']).factory('spyAPI', [ | |
'$rootScope', | |
'scrollContainerAPI', | |
function ($rootScope, scrollContainerAPI) { | |
var createScrollHandler = function (context) { | |
return function () { | |
var container = context.container, containerEl = container[0], containerOffset = 0; | |
if (containerEl instanceof HTMLElement) { | |
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) | |
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; | |
}; | |
}; | |
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 addSpy = function (spy) { | |
var context = getContextForSpy(spy); | |
getContextForSpy(spy).spies.push(spy); | |
if (!context.container) { | |
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) { | |
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', | |
'scrollContainerAPI', | |
function (duScrollDuration, scrollContainerAPI) { | |
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) : 0; | |
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) { | |
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) { | |
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', | |
'$timeout', | |
function (spyAPI, $timeout) { | |
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) : 0)); | |
spyAPI.addSpy(spy); | |
$scope.$on('$destroy', function () { | |
spyAPI.removeSpy(spy); | |
}); | |
$scope.$on('$locationChangeSuccess', spy.flushTargetCache.bind(spy)); | |
$scope.$on('$stateChangeSuccess', spy.flushTargetCache.bind(spy)); | |
}, 0); | |
} | |
}; | |
} | |
]); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment