scrollspy.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. /**
  2. * angular-strap
  3. * @version v2.3.9 - 2016-06-10
  4. * @link http://mgcrea.github.io/angular-strap
  5. * @author Olivier Louvignes <olivier@mg-crea.com> (https://github.com/mgcrea)
  6. * @license MIT License, http://www.opensource.org/licenses/MIT
  7. */
  8. 'use strict';
  9. angular.module('mgcrea.ngStrap.scrollspy', [ 'mgcrea.ngStrap.helpers.debounce', 'mgcrea.ngStrap.helpers.dimensions' ]).provider('$scrollspy', function() {
  10. var spies = this.$$spies = {};
  11. var defaults = this.defaults = {
  12. debounce: 150,
  13. throttle: 100,
  14. offset: 100
  15. };
  16. this.$get = [ '$window', '$document', '$rootScope', 'dimensions', 'debounce', 'throttle', function($window, $document, $rootScope, dimensions, debounce, throttle) {
  17. var windowEl = angular.element($window);
  18. var docEl = angular.element($document.prop('documentElement'));
  19. var bodyEl = angular.element($window.document.body);
  20. function nodeName(element, name) {
  21. return element[0].nodeName && element[0].nodeName.toLowerCase() === name.toLowerCase();
  22. }
  23. function ScrollSpyFactory(config) {
  24. var options = angular.extend({}, defaults, config);
  25. if (!options.element) options.element = bodyEl;
  26. var isWindowSpy = nodeName(options.element, 'body');
  27. var scrollEl = isWindowSpy ? windowEl : options.element;
  28. var scrollId = isWindowSpy ? 'window' : options.id;
  29. if (spies[scrollId]) {
  30. spies[scrollId].$$count++;
  31. return spies[scrollId];
  32. }
  33. var $scrollspy = {};
  34. var unbindViewContentLoaded;
  35. var unbindIncludeContentLoaded;
  36. var trackedElements = $scrollspy.$trackedElements = [];
  37. var sortedElements = [];
  38. var activeTarget;
  39. var debouncedCheckPosition;
  40. var throttledCheckPosition;
  41. var debouncedCheckOffsets;
  42. var viewportHeight;
  43. var scrollTop;
  44. $scrollspy.init = function() {
  45. this.$$count = 1;
  46. debouncedCheckPosition = debounce(this.checkPosition, options.debounce);
  47. throttledCheckPosition = throttle(this.checkPosition, options.throttle);
  48. scrollEl.on('click', this.checkPositionWithEventLoop);
  49. windowEl.on('resize', debouncedCheckPosition);
  50. scrollEl.on('scroll', throttledCheckPosition);
  51. debouncedCheckOffsets = debounce(this.checkOffsets, options.debounce);
  52. unbindViewContentLoaded = $rootScope.$on('$viewContentLoaded', debouncedCheckOffsets);
  53. unbindIncludeContentLoaded = $rootScope.$on('$includeContentLoaded', debouncedCheckOffsets);
  54. debouncedCheckOffsets();
  55. if (scrollId) {
  56. spies[scrollId] = $scrollspy;
  57. }
  58. };
  59. $scrollspy.destroy = function() {
  60. this.$$count--;
  61. if (this.$$count > 0) {
  62. return;
  63. }
  64. scrollEl.off('click', this.checkPositionWithEventLoop);
  65. windowEl.off('resize', debouncedCheckPosition);
  66. scrollEl.off('scroll', throttledCheckPosition);
  67. unbindViewContentLoaded();
  68. unbindIncludeContentLoaded();
  69. if (scrollId) {
  70. delete spies[scrollId];
  71. }
  72. };
  73. $scrollspy.checkPosition = function() {
  74. if (!sortedElements.length) return;
  75. scrollTop = (isWindowSpy ? $window.pageYOffset : scrollEl.prop('scrollTop')) || 0;
  76. viewportHeight = Math.max($window.innerHeight, docEl.prop('clientHeight'));
  77. if (scrollTop < sortedElements[0].offsetTop && activeTarget !== sortedElements[0].target) {
  78. return $scrollspy.$activateElement(sortedElements[0]);
  79. }
  80. for (var i = sortedElements.length; i--; ) {
  81. if (angular.isUndefined(sortedElements[i].offsetTop) || sortedElements[i].offsetTop === null) continue;
  82. if (activeTarget === sortedElements[i].target) continue;
  83. if (scrollTop < sortedElements[i].offsetTop) continue;
  84. if (sortedElements[i + 1] && scrollTop > sortedElements[i + 1].offsetTop) continue;
  85. return $scrollspy.$activateElement(sortedElements[i]);
  86. }
  87. };
  88. $scrollspy.checkPositionWithEventLoop = function() {
  89. setTimeout($scrollspy.checkPosition, 1);
  90. };
  91. $scrollspy.$activateElement = function(element) {
  92. if (activeTarget) {
  93. var activeElement = $scrollspy.$getTrackedElement(activeTarget);
  94. if (activeElement) {
  95. activeElement.source.removeClass('active');
  96. if (nodeName(activeElement.source, 'li') && nodeName(activeElement.source.parent().parent(), 'li')) {
  97. activeElement.source.parent().parent().removeClass('active');
  98. }
  99. }
  100. }
  101. activeTarget = element.target;
  102. element.source.addClass('active');
  103. if (nodeName(element.source, 'li') && nodeName(element.source.parent().parent(), 'li')) {
  104. element.source.parent().parent().addClass('active');
  105. }
  106. };
  107. $scrollspy.$getTrackedElement = function(target) {
  108. return trackedElements.filter(function(obj) {
  109. return obj.target === target;
  110. })[0];
  111. };
  112. $scrollspy.checkOffsets = function() {
  113. angular.forEach(trackedElements, function(trackedElement) {
  114. var targetElement = document.querySelector(trackedElement.target);
  115. trackedElement.offsetTop = targetElement ? dimensions.offset(targetElement).top : null;
  116. if (options.offset && trackedElement.offsetTop !== null) trackedElement.offsetTop -= options.offset * 1;
  117. });
  118. sortedElements = trackedElements.filter(function(el) {
  119. return el.offsetTop !== null;
  120. }).sort(function(a, b) {
  121. return a.offsetTop - b.offsetTop;
  122. });
  123. debouncedCheckPosition();
  124. };
  125. $scrollspy.trackElement = function(target, source) {
  126. trackedElements.push({
  127. target: target,
  128. source: source
  129. });
  130. };
  131. $scrollspy.untrackElement = function(target, source) {
  132. var toDelete;
  133. for (var i = trackedElements.length; i--; ) {
  134. if (trackedElements[i].target === target && trackedElements[i].source === source) {
  135. toDelete = i;
  136. break;
  137. }
  138. }
  139. trackedElements.splice(toDelete, 1);
  140. };
  141. $scrollspy.activate = function(i) {
  142. trackedElements[i].addClass('active');
  143. };
  144. $scrollspy.init();
  145. return $scrollspy;
  146. }
  147. return ScrollSpyFactory;
  148. } ];
  149. }).directive('bsScrollspy', [ '$rootScope', 'debounce', 'dimensions', '$scrollspy', function($rootScope, debounce, dimensions, $scrollspy) {
  150. return {
  151. restrict: 'EAC',
  152. link: function postLink(scope, element, attr) {
  153. var options = {
  154. scope: scope
  155. };
  156. angular.forEach([ 'offset', 'target' ], function(key) {
  157. if (angular.isDefined(attr[key])) options[key] = attr[key];
  158. });
  159. var scrollspy = $scrollspy(options);
  160. scrollspy.trackElement(options.target, element);
  161. scope.$on('$destroy', function() {
  162. if (scrollspy) {
  163. scrollspy.untrackElement(options.target, element);
  164. scrollspy.destroy();
  165. }
  166. options = null;
  167. scrollspy = null;
  168. });
  169. }
  170. };
  171. } ]).directive('bsScrollspyList', [ '$rootScope', 'debounce', 'dimensions', '$scrollspy', function($rootScope, debounce, dimensions, $scrollspy) {
  172. return {
  173. restrict: 'A',
  174. compile: function postLink(element, attr) {
  175. var children = element[0].querySelectorAll('li > a[href]');
  176. angular.forEach(children, function(child) {
  177. var childEl = angular.element(child);
  178. childEl.parent().attr('bs-scrollspy', '').attr('data-target', childEl.attr('href'));
  179. });
  180. }
  181. };
  182. } ]);