Things that I'm not sure about:
- Whether this works in all use cases - alongside routing and within templates etc
- Am I polluting the scope with all these variables? This seems to be the easiest way to do unit tests - or should I be passing the vars into the function? Some ( documentHeight, USerScrolledTop, userScrolledBottom) are also generic to all - should they be contained / updated separately?
- Should I implement a scroll throttle?
- Logic assumes elements are written in order in the HTML (last directive hit will be overwritting the current
activeSpy
value) - Is the broadcast/listen logic satisfactory, or should I specifically broadcast the
id
rather than the genericspied
, removing the need forif( scope.scrollspyListen === args.activeSpy )
- Things I don't know I should be unsure about
Love to get some feedback on this module. Feel free to fork and whatnot over on GitHub as well. Demo.
<nav>
<span class="item" data-scrollspy-listen="newyork">New York</span>
<span class="item" data-scrollspy-listen="london">London</span>
<span class="item" data-scrollspy-listen="sydney">Sydney</span>
</nav>
<section id="newyork" data-scrollspy-broadcast></section>
<section id="london" data-scrollspy-broadcast></section>
<section id="sydney" data-scrollspy-broadcast></section>
'use strict';
angular.module( 'ngScrollSpy', [] )
.directive( 'scrollspyBroadcast', [ '$rootScope', function( $rootScope ) {
return {
restrict: 'A',
scope: {},
link: function( scope, element, attrs ) {
scope.activate = function() {
scope.documentHeight = Math.max( document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight );
//distance down the page the top of the window is currently at
scope.userScrolledTop = ( window.pageYOffset !== undefined ) ? window.pageYOffset : ( document.documentElement || document.body.parentNode || document.body ).scrollTop;
//distance down the page the bottom of the window is currently at
scope.userScrolledBottom = scope.userScrolledTop + window.innerHeight;
scope.elementOffsetTop = element[0].offsetTop;
scope.elementOffsetBottom = scope.elementOffsetTop + Math.max( element[0].scrollHeight, element[0].offsetHeight );
scope.triggerOffset = 150;
//determine if element needs to be triggered by the top or bottom of the window
if( ( scope.elementOffsetTop - scope.triggerOffset ) < ( scope.documentHeight - window.innerHeight ) ) {
if( scope.elementOffsetTop <= ( scope.userScrolledTop + scope.triggerOffset ) ) {
$rootScope.$broadcast( 'spied', {
'activeSpy': attrs.id
});
}
} else {
if( scope.userScrolledBottom > ( scope.elementOffsetBottom - scope.triggerOffset ) ) {
$rootScope.$broadcast( 'spied', {
'activeSpy': attrs.id
});
}
}
};
angular.element( document ).ready( function() {
scope.activate();
});
angular.element( window ).bind( 'scroll', function() {
scope.activate();
});
}
}
}])
.directive( 'scrollspyListen', [ '$rootScope', function( $rootScope ) {
return {
restrict: 'A',
scope: {
scrollspyListen: '@',
enabled: '@'
},
replace: true,
transclude: true,
template: function( element, attrs ) {
var tag = element[0].nodeName;
return '<'+tag+' data-ng-transclude data-ng-class="{active: enabled}"></'+tag+'>';
},
link: function( scope, element, attrs ) {
$rootScope.$on('spied', function(event, args){
scope.enabled = false;
if( scope.scrollspyListen === args.activeSpy ) {
scope.enabled = true;
}
if( !scope.$$phase ) scope.$digest();
});
}
}
}]);