262 lines
8.5 KiB
JavaScript
262 lines
8.5 KiB
JavaScript
/**
|
|
* angular-strap
|
|
* @version v2.2.1 - 2015-03-10
|
|
* @link http://mgcrea.github.io/angular-strap
|
|
* @author Olivier Louvignes (olivier@mg-crea.com)
|
|
* @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() {
|
|
|
|
// Pool of registered spies
|
|
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);
|
|
|
|
// Helper functions
|
|
|
|
function nodeName(element, name) {
|
|
return element[0].nodeName && element[0].nodeName.toLowerCase() === name.toLowerCase();
|
|
}
|
|
|
|
function ScrollSpyFactory(config) {
|
|
|
|
// Common vars
|
|
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;
|
|
|
|
// Use existing spy
|
|
if(spies[scrollId]) {
|
|
spies[scrollId].$$count++;
|
|
return spies[scrollId];
|
|
}
|
|
|
|
var $scrollspy = {};
|
|
|
|
// Private vars
|
|
var unbindViewContentLoaded, unbindIncludeContentLoaded;
|
|
var trackedElements = $scrollspy.$trackedElements = [];
|
|
var sortedElements = [];
|
|
var activeTarget;
|
|
var debouncedCheckPosition;
|
|
var throttledCheckPosition;
|
|
var debouncedCheckOffsets;
|
|
var viewportHeight;
|
|
var scrollTop;
|
|
|
|
$scrollspy.init = function() {
|
|
|
|
// Setup internal ref counter
|
|
this.$$count = 1;
|
|
|
|
// Bind events
|
|
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();
|
|
|
|
// Register spy for reuse
|
|
if(scrollId) {
|
|
spies[scrollId] = $scrollspy;
|
|
}
|
|
|
|
};
|
|
|
|
$scrollspy.destroy = function() {
|
|
|
|
// Check internal ref counter
|
|
this.$$count--;
|
|
if(this.$$count > 0) {
|
|
return;
|
|
}
|
|
|
|
// Unbind events
|
|
scrollEl.off('click', this.checkPositionWithEventLoop);
|
|
windowEl.off('resize', debouncedCheckPosition);
|
|
scrollEl.off('scroll', throttledCheckPosition);
|
|
unbindViewContentLoaded();
|
|
unbindIncludeContentLoaded();
|
|
if (scrollId) {
|
|
delete spies[scrollId];
|
|
}
|
|
};
|
|
|
|
$scrollspy.checkPosition = function() {
|
|
|
|
// Not ready yet
|
|
if(!sortedElements.length) return;
|
|
|
|
// Calculate the scroll position
|
|
scrollTop = (isWindowSpy ? $window.pageYOffset : scrollEl.prop('scrollTop')) || 0;
|
|
|
|
// Calculate the viewport height for use by the components
|
|
viewportHeight = Math.max($window.innerHeight, docEl.prop('clientHeight'));
|
|
|
|
// Activate first element if scroll is smaller
|
|
if(scrollTop < sortedElements[0].offsetTop && activeTarget !== sortedElements[0].target) {
|
|
return $scrollspy.$activateElement(sortedElements[0]);
|
|
}
|
|
|
|
// Activate proper element
|
|
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() {
|
|
// IE 9 throws an error if we use 'this' instead of '$scrollspy'
|
|
// in this setTimeout call
|
|
setTimeout($scrollspy.checkPosition, 1);
|
|
};
|
|
|
|
// Protected methods
|
|
|
|
$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];
|
|
};
|
|
|
|
// Track offsets behavior
|
|
|
|
$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 = trackedElements.splice(toDelete, 1);
|
|
};
|
|
|
|
$scrollspy.activate = function(i) {
|
|
trackedElements[i].addClass('active');
|
|
};
|
|
|
|
// Initialize plugin
|
|
|
|
$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'));
|
|
});
|
|
}
|
|
|
|
};
|
|
|
|
}]);
|