scrollspy.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. /**
  2. * angular-strap
  3. * @version v2.3.5 - 2015-10-29
  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, unbindIncludeContentLoaded;
  35. var trackedElements = $scrollspy.$trackedElements = [];
  36. var sortedElements = [];
  37. var activeTarget;
  38. var debouncedCheckPosition;
  39. var throttledCheckPosition;
  40. var debouncedCheckOffsets;
  41. var viewportHeight;
  42. var scrollTop;
  43. $scrollspy.init = function() {
  44. this.$$count = 1;
  45. debouncedCheckPosition = debounce(this.checkPosition, options.debounce);
  46. throttledCheckPosition = throttle(this.checkPosition, options.throttle);
  47. scrollEl.on('click', this.checkPositionWithEventLoop);
  48. windowEl.on('resize', debouncedCheckPosition);
  49. scrollEl.on('scroll', throttledCheckPosition);
  50. debouncedCheckOffsets = debounce(this.checkOffsets, options.debounce);
  51. unbindViewContentLoaded = $rootScope.$on('$viewContentLoaded', debouncedCheckOffsets);
  52. unbindIncludeContentLoaded = $rootScope.$on('$includeContentLoaded', debouncedCheckOffsets);
  53. debouncedCheckOffsets();
  54. if (scrollId) {
  55. spies[scrollId] = $scrollspy;
  56. }
  57. };
  58. $scrollspy.destroy = function() {
  59. this.$$count--;
  60. if (this.$$count > 0) {
  61. return;
  62. }
  63. scrollEl.off('click', this.checkPositionWithEventLoop);
  64. windowEl.off('resize', debouncedCheckPosition);
  65. scrollEl.off('scroll', throttledCheckPosition);
  66. unbindViewContentLoaded();
  67. unbindIncludeContentLoaded();
  68. if (scrollId) {
  69. delete spies[scrollId];
  70. }
  71. };
  72. $scrollspy.checkPosition = function() {
  73. if (!sortedElements.length) return;
  74. scrollTop = (isWindowSpy ? $window.pageYOffset : scrollEl.prop('scrollTop')) || 0;
  75. viewportHeight = Math.max($window.innerHeight, docEl.prop('clientHeight'));
  76. if (scrollTop < sortedElements[0].offsetTop && activeTarget !== sortedElements[0].target) {
  77. return $scrollspy.$activateElement(sortedElements[0]);
  78. }
  79. for (var i = sortedElements.length; i--; ) {
  80. if (angular.isUndefined(sortedElements[i].offsetTop) || sortedElements[i].offsetTop === null) continue;
  81. if (activeTarget === sortedElements[i].target) continue;
  82. if (scrollTop < sortedElements[i].offsetTop) continue;
  83. if (sortedElements[i + 1] && scrollTop > sortedElements[i + 1].offsetTop) continue;
  84. return $scrollspy.$activateElement(sortedElements[i]);
  85. }
  86. };
  87. $scrollspy.checkPositionWithEventLoop = function() {
  88. setTimeout($scrollspy.checkPosition, 1);
  89. };
  90. $scrollspy.$activateElement = function(element) {
  91. if (activeTarget) {
  92. var activeElement = $scrollspy.$getTrackedElement(activeTarget);
  93. if (activeElement) {
  94. activeElement.source.removeClass('active');
  95. if (nodeName(activeElement.source, 'li') && nodeName(activeElement.source.parent().parent(), 'li')) {
  96. activeElement.source.parent().parent().removeClass('active');
  97. }
  98. }
  99. }
  100. activeTarget = element.target;
  101. element.source.addClass('active');
  102. if (nodeName(element.source, 'li') && nodeName(element.source.parent().parent(), 'li')) {
  103. element.source.parent().parent().addClass('active');
  104. }
  105. };
  106. $scrollspy.$getTrackedElement = function(target) {
  107. return trackedElements.filter(function(obj) {
  108. return obj.target === target;
  109. })[0];
  110. };
  111. $scrollspy.checkOffsets = function() {
  112. angular.forEach(trackedElements, function(trackedElement) {
  113. var targetElement = document.querySelector(trackedElement.target);
  114. trackedElement.offsetTop = targetElement ? dimensions.offset(targetElement).top : null;
  115. if (options.offset && trackedElement.offsetTop !== null) trackedElement.offsetTop -= options.offset * 1;
  116. });
  117. sortedElements = trackedElements.filter(function(el) {
  118. return el.offsetTop !== null;
  119. }).sort(function(a, b) {
  120. return a.offsetTop - b.offsetTop;
  121. });
  122. debouncedCheckPosition();
  123. };
  124. $scrollspy.trackElement = function(target, source) {
  125. trackedElements.push({
  126. target: target,
  127. source: source
  128. });
  129. };
  130. $scrollspy.untrackElement = function(target, source) {
  131. var toDelete;
  132. for (var i = trackedElements.length; i--; ) {
  133. if (trackedElements[i].target === target && trackedElements[i].source === source) {
  134. toDelete = i;
  135. break;
  136. }
  137. }
  138. trackedElements = trackedElements.splice(toDelete, 1);
  139. };
  140. $scrollspy.activate = function(i) {
  141. trackedElements[i].addClass('active');
  142. };
  143. $scrollspy.init();
  144. return $scrollspy;
  145. }
  146. return ScrollSpyFactory;
  147. } ];
  148. }).directive('bsScrollspy', [ '$rootScope', 'debounce', 'dimensions', '$scrollspy', function($rootScope, debounce, dimensions, $scrollspy) {
  149. return {
  150. restrict: 'EAC',
  151. link: function postLink(scope, element, attr) {
  152. var options = {
  153. scope: scope
  154. };
  155. angular.forEach([ 'offset', 'target' ], function(key) {
  156. if (angular.isDefined(attr[key])) options[key] = attr[key];
  157. });
  158. var scrollspy = $scrollspy(options);
  159. scrollspy.trackElement(options.target, element);
  160. scope.$on('$destroy', function() {
  161. if (scrollspy) {
  162. scrollspy.untrackElement(options.target, element);
  163. scrollspy.destroy();
  164. }
  165. options = null;
  166. scrollspy = null;
  167. });
  168. }
  169. };
  170. } ]).directive('bsScrollspyList', [ '$rootScope', 'debounce', 'dimensions', '$scrollspy', function($rootScope, debounce, dimensions, $scrollspy) {
  171. return {
  172. restrict: 'A',
  173. compile: function postLink(element, attr) {
  174. var children = element[0].querySelectorAll('li > a[href]');
  175. angular.forEach(children, function(child) {
  176. var childEl = angular.element(child);
  177. childEl.parent().attr('bs-scrollspy', '').attr('data-target', childEl.attr('href'));
  178. });
  179. }
  180. };
  181. } ]);