Skip to content

Instantly share code, notes, and snippets.

@bcabanes
Last active September 26, 2015 23:13
Show Gist options
  • Save bcabanes/4a87651e55509deca31c to your computer and use it in GitHub Desktop.
Save bcabanes/4a87651e55509deca31c to your computer and use it in GitHub Desktop.
Angular sticky container
/**
* Angular sticky directive
* The directive is used by attribute [sticky].
*/
(function (angular) {
'use strict';
angular
.module('sticky', [])
.directive('sticky', directive);
function directive() {
return {
restrict: 'A',
link: link
};
function link(scope, element, attributes) {
var mediaQuery,
stickyClass,
bodyClass,
_container,
_window,
_body,
_document,
initialCSS,
initialStyle,
isPositionFixed,
isSticking,
stickyLine,
offset,
anchor,
prevOffset,
matchMedia;
isPositionFixed = false;
isSticking = false;
matchMedia = window.matchMedia;
// elements
_window = angular.element(window);
_body = angular.element(document.body);
_container = element[0];
_document = document.documentElement;
// attributes
mediaQuery = attributes.mediaQuery || false;
stickyClass = attributes.stickyClass || '';
bodyClass = attributes.bodyClass || '';
initialStyle = element.attr('style');
offset = typeof attributes.offset === 'string' ?
parseInt(attributes.offset.replace(/px;?/, '')) :
0;
anchor = typeof attributes.anchor === 'string' ?
attributes.anchor.toLowerCase().trim()
: 'top';
// initial style
initialCSS = {
top: element.css('top'),
width: element.css('width'),
position: element.css('position'),
marginTop: element.css('margin-top'),
cssLeft: element.css('left')
};
switch (anchor) {
case 'top':
case 'bottom':
break;
default:
console.log('Unknown anchor '+anchor+', defaulting to top');
anchor = 'top';
break;
}
// Listeners
//
_window.on('scroll', checkIfShouldStick);
_window.on('resize', scope.$apply.bind(scope, onResize));
scope.$on('$destroy', onDestroy);
function onResize() {
initialCSS.offsetWidth = _container.offsetWidth;
checkIfShouldStick();
if(isSticking){
var parent = window.getComputedStyle(_container.parentElement, null),
initialOffsetWidth = _container.parentElement.offsetWidth -
parent.getPropertyValue('padding-right').replace('px', '') -
parent.getPropertyValue('padding-left').replace('px', '');
element.css('width', initialOffsetWidth+'px');
}
}
function onDestroy() {
_window.off('scroll', checkIfShouldStick);
_window.off('resize', onResize);
if ( bodyClass ) {
_body.removeClass(bodyClass);
}
}
// Watcher
//
prevOffset = _getTopOffset(_container);
scope.$watch( function() { // triggered on load and on digest cycle
if (element.width() === 0) {
unstickElement();
}
if ( isSticking ) return prevOffset;
prevOffset =
(anchor === 'top') ?
_getTopOffset(_container) :
_getBottomOffset(_container);
return prevOffset;
}, function(newVal, oldVal) {
if ( newVal !== oldVal || typeof stickyLine === 'undefined' ) {
stickyLine = newVal - offset;
checkIfShouldStick();
}
});
// Methods
//
function checkIfShouldStick() {
var scrollTop, shouldStick, scrollBottom, scrolledDistance;
if ( mediaQuery && !matchMedia('('+mediaQuery+')').matches)
return;
if ( anchor === 'top' ) {
scrolledDistance = window.pageYOffset || _document.scrollTop;
scrollTop = scrolledDistance - (_document.clientTop || 0);
shouldStick = scrollTop >= stickyLine;
} else {
scrollBottom = window.pageYOffset + window.innerHeight;
shouldStick = scrollBottom <= stickyLine;
}
// Switch the sticky mode if the element crosses the sticky line
if ( shouldStick && !isSticking )
stickElement();
else if ( !shouldStick && isSticking )
unstickElement();
}
function stickElement() {
var rect, absoluteLeft;
rect = element[0].getBoundingClientRect();
absoluteLeft = rect.left;
initialCSS.offsetWidth = _container.offsetWidth;
isSticking = true;
if ( bodyClass ) {
_body.addClass(bodyClass);
}
if ( stickyClass ) {
element.addClass(stickyClass);
}
element
.css('width', _container.offsetWidth+'px')
.css('position', 'fixed')
.css(anchor, offset+'px')
.css('left', absoluteLeft)
.css('margin-top', 0);
if ( anchor === 'bottom' ) {
element.css('margin-bottom', 0);
}
}
function unstickElement() {
element.attr('style', element.initialStyle);
isSticking = false;
if ( bodyClass ) {
_body.removeClass(bodyClass);
}
if ( stickyClass ) {
element.removeClass(stickyClass);
}
element
.css('width', '')
.css('top', initialCSS.top)
.css('position', initialCSS.position)
.css('left', initialCSS.cssLeft)
.css('margin-top', initialCSS.marginTop);
}
function _getTopOffset (element) {
var pixels = 0;
if (element.offsetParent) {
do {
pixels += element.offsetTop;
element = element.offsetParent;
} while (element);
}
return pixels;
}
function _getBottomOffset (element) {
return element.offsetTop + element.clientHeight;
}
}
}
// Shiv: matchMedia
//
window.matchMedia = window.matchMedia || (function() {
var warning = 'angular-sticky: This browser does not support '+
'matchMedia, therefore the minWidth option will not work on '+
'this browser. Polyfill matchMedia to fix this issue.';
if ( window.console && console.warn ) {
console.warn(warning);
}
return function() {
return {
matches: true
};
};
}());
})(angular);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment