angular-scroll.js 17 KB

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