/** * angular-strap * @version v2.3.9 - 2016-06-10 * @link http://mgcrea.github.io/angular-strap * @author Olivier Louvignes (https://github.com/mgcrea) * @license MIT License, http://www.opensource.org/licenses/MIT */ 'use strict'; angular.module('mgcrea.ngStrap.scrollspy', [ 'mgcrea.ngStrap.helpers.debounce', 'mgcrea.ngStrap.helpers.dimensions' ]).provider('$scrollspy', function() { var spies = this.$$spies = {}; var defaults = this.defaults = { debounce: 150, throttle: 100, offset: 100 }; this.$get = [ '$window', '$document', '$rootScope', 'dimensions', 'debounce', 'throttle', function($window, $document, $rootScope, dimensions, debounce, throttle) { var windowEl = angular.element($window); var docEl = angular.element($document.prop('documentElement')); var bodyEl = angular.element($window.document.body); function nodeName(element, name) { return element[0].nodeName && element[0].nodeName.toLowerCase() === name.toLowerCase(); } function ScrollSpyFactory(config) { var options = angular.extend({}, defaults, config); if (!options.element) options.element = bodyEl; var isWindowSpy = nodeName(options.element, 'body'); var scrollEl = isWindowSpy ? windowEl : options.element; var scrollId = isWindowSpy ? 'window' : options.id; if (spies[scrollId]) { spies[scrollId].$$count++; return spies[scrollId]; } var $scrollspy = {}; var unbindViewContentLoaded; var unbindIncludeContentLoaded; var trackedElements = $scrollspy.$trackedElements = []; var sortedElements = []; var activeTarget; var debouncedCheckPosition; var throttledCheckPosition; var debouncedCheckOffsets; var viewportHeight; var scrollTop; $scrollspy.init = function() { this.$$count = 1; debouncedCheckPosition = debounce(this.checkPosition, options.debounce); throttledCheckPosition = throttle(this.checkPosition, options.throttle); scrollEl.on('click', this.checkPositionWithEventLoop); windowEl.on('resize', debouncedCheckPosition); scrollEl.on('scroll', throttledCheckPosition); debouncedCheckOffsets = debounce(this.checkOffsets, options.debounce); unbindViewContentLoaded = $rootScope.$on('$viewContentLoaded', debouncedCheckOffsets); unbindIncludeContentLoaded = $rootScope.$on('$includeContentLoaded', debouncedCheckOffsets); debouncedCheckOffsets(); if (scrollId) { spies[scrollId] = $scrollspy; } }; $scrollspy.destroy = function() { this.$$count--; if (this.$$count > 0) { return; } scrollEl.off('click', this.checkPositionWithEventLoop); windowEl.off('resize', debouncedCheckPosition); scrollEl.off('scroll', throttledCheckPosition); unbindViewContentLoaded(); unbindIncludeContentLoaded(); if (scrollId) { delete spies[scrollId]; } }; $scrollspy.checkPosition = function() { if (!sortedElements.length) return; scrollTop = (isWindowSpy ? $window.pageYOffset : scrollEl.prop('scrollTop')) || 0; viewportHeight = Math.max($window.innerHeight, docEl.prop('clientHeight')); if (scrollTop < sortedElements[0].offsetTop && activeTarget !== sortedElements[0].target) { return $scrollspy.$activateElement(sortedElements[0]); } for (var i = sortedElements.length; i--; ) { if (angular.isUndefined(sortedElements[i].offsetTop) || sortedElements[i].offsetTop === null) continue; if (activeTarget === sortedElements[i].target) continue; if (scrollTop < sortedElements[i].offsetTop) continue; if (sortedElements[i + 1] && scrollTop > sortedElements[i + 1].offsetTop) continue; return $scrollspy.$activateElement(sortedElements[i]); } }; $scrollspy.checkPositionWithEventLoop = function() { setTimeout($scrollspy.checkPosition, 1); }; $scrollspy.$activateElement = function(element) { if (activeTarget) { var activeElement = $scrollspy.$getTrackedElement(activeTarget); if (activeElement) { activeElement.source.removeClass('active'); if (nodeName(activeElement.source, 'li') && nodeName(activeElement.source.parent().parent(), 'li')) { activeElement.source.parent().parent().removeClass('active'); } } } activeTarget = element.target; element.source.addClass('active'); if (nodeName(element.source, 'li') && nodeName(element.source.parent().parent(), 'li')) { element.source.parent().parent().addClass('active'); } }; $scrollspy.$getTrackedElement = function(target) { return trackedElements.filter(function(obj) { return obj.target === target; })[0]; }; $scrollspy.checkOffsets = function() { angular.forEach(trackedElements, function(trackedElement) { var targetElement = document.querySelector(trackedElement.target); trackedElement.offsetTop = targetElement ? dimensions.offset(targetElement).top : null; if (options.offset && trackedElement.offsetTop !== null) trackedElement.offsetTop -= options.offset * 1; }); sortedElements = trackedElements.filter(function(el) { return el.offsetTop !== null; }).sort(function(a, b) { return a.offsetTop - b.offsetTop; }); debouncedCheckPosition(); }; $scrollspy.trackElement = function(target, source) { trackedElements.push({ target: target, source: source }); }; $scrollspy.untrackElement = function(target, source) { var toDelete; for (var i = trackedElements.length; i--; ) { if (trackedElements[i].target === target && trackedElements[i].source === source) { toDelete = i; break; } } trackedElements.splice(toDelete, 1); }; $scrollspy.activate = function(i) { trackedElements[i].addClass('active'); }; $scrollspy.init(); return $scrollspy; } return ScrollSpyFactory; } ]; }).directive('bsScrollspy', [ '$rootScope', 'debounce', 'dimensions', '$scrollspy', function($rootScope, debounce, dimensions, $scrollspy) { return { restrict: 'EAC', link: function postLink(scope, element, attr) { var options = { scope: scope }; angular.forEach([ 'offset', 'target' ], function(key) { if (angular.isDefined(attr[key])) options[key] = attr[key]; }); var scrollspy = $scrollspy(options); scrollspy.trackElement(options.target, element); scope.$on('$destroy', function() { if (scrollspy) { scrollspy.untrackElement(options.target, element); scrollspy.destroy(); } options = null; scrollspy = null; }); } }; } ]).directive('bsScrollspyList', [ '$rootScope', 'debounce', 'dimensions', '$scrollspy', function($rootScope, debounce, dimensions, $scrollspy) { return { restrict: 'A', compile: function postLink(element, attr) { var children = element[0].querySelectorAll('li > a[href]'); angular.forEach(children, function(child) { var childEl = angular.element(child); childEl.parent().attr('bs-scrollspy', '').attr('data-target', childEl.attr('href')); }); } }; } ]);