src/affix/affix.js
'use strict';
angular.module('mgcrea.ngStrap.affix', ['mgcrea.ngStrap.helpers.dimensions', 'mgcrea.ngStrap.helpers.debounce'])
.provider('$affix', function () {
var defaults = this.defaults = {
offsetTop: 'auto',
inlineStyles: true,
setWidth: true
};
this.$get = function ($window, debounce, dimensions) {
var documentEl = angular.element($window.document);
var windowEl = angular.element($window);
function AffixFactory (element, config) {
var $affix = {};
// Common vars
var options = angular.extend({}, defaults, config);
var targetEl = options.target;
// Initial private vars
var reset = 'affix affix-top affix-bottom';
var setWidth = false;
var initialAffixTop = 0;
var initialOffsetTop = 0;
var offsetTop = 0;
var offsetBottom = 0;
var affixed = null;
var unpin = null;
var parent = element.parent();
// Options: custom parent
if (options.offsetParent) {
if (options.offsetParent.match(/^\d+$/)) {
for (var i = 0; i < (options.offsetParent * 1) - 1; i++) {
parent = parent.parent();
}
} else {
parent = angular.element(options.offsetParent);
}
}
$affix.init = function () {
this.$parseOffsets();
initialOffsetTop = dimensions.offset(element[0]).top + initialAffixTop;
setWidth = options.setWidth && !element[0].style.width;
// Bind events
targetEl.on('scroll', this.checkPosition);
targetEl.on('click', this.checkPositionWithEventLoop);
windowEl.on('resize', this.$debouncedOnResize);
// Both of these checkPosition() calls are necessary for the case where
// the user hits refresh after scrolling to the bottom of the page.
this.checkPosition();
this.checkPositionWithEventLoop();
};
$affix.destroy = function () {
// Unbind events
targetEl.off('scroll', this.checkPosition);
targetEl.off('click', this.checkPositionWithEventLoop);
windowEl.off('resize', this.$debouncedOnResize);
};
$affix.checkPositionWithEventLoop = function () {
// IE 9 throws an error if we use 'this' instead of '$affix'
// in this setTimeout call
setTimeout($affix.checkPosition, 1);
};
$affix.checkPosition = function () {
// if (!this.$element.is(':visible')) return
var scrollTop = getScrollTop();
var position = dimensions.offset(element[0]);
var elementHeight = dimensions.height(element[0]);
// Get required affix class according to position
var affix = getRequiredAffixClass(unpin, position, elementHeight);
// Did affix status changed this last check?
if (affixed === affix) return;
affixed = affix;
if (affix === 'top') {
unpin = null;
if (setWidth) {
element.css('width', '');
}
if (options.inlineStyles) {
element.css('position', (options.offsetParent) ? '' : 'relative');
element.css('top', '');
}
} else if (affix === 'bottom') {
if (options.offsetUnpin) {
unpin = -(options.offsetUnpin * 1);
} else {
// Calculate unpin threshold when affixed to bottom.
// Hopefully the browser scrolls pixel by pixel.
unpin = position.top - scrollTop;
}
if (setWidth) {
element.css('width', '');
}
if (options.inlineStyles) {
element.css('position', (options.offsetParent) ? '' : 'relative');
element.css('top', (options.offsetParent) ? '' : ((documentEl.height() - offsetBottom - elementHeight - initialOffsetTop) + 'px'));
}
} else { // affix === 'middle'
unpin = null;
if (setWidth) {
element.css('width', element[0].offsetWidth + 'px');
}
if (options.inlineStyles) {
element.css('position', 'fixed');
element.css('top', initialAffixTop + 'px');
}
}
// Add proper affix class
element.removeClass(reset).addClass('affix' + ((affix !== 'middle') ? '-' + affix : ''));
};
$affix.$onResize = function () {
$affix.$parseOffsets();
$affix.checkPosition();
};
$affix.$debouncedOnResize = debounce($affix.$onResize, 50);
$affix.$parseOffsets = function () {
var initialPosition = element[0].style.position;
var initialTop = element[0].style.top;
// Reset position to calculate correct offsetTop
if (options.inlineStyles) {
element.css('position', (options.offsetParent) ? '' : 'relative');
element.css('top', '');
}
if (options.offsetTop) {
if (options.offsetTop === 'auto') {
options.offsetTop = '+0';
}
if (options.offsetTop.match(/^[-+]\d+$/)) {
initialAffixTop = - options.offsetTop * 1;
if (options.offsetParent) {
offsetTop = dimensions.offset(parent[0]).top + (options.offsetTop * 1);
} else {
offsetTop = dimensions.offset(element[0]).top - dimensions.css(element[0], 'marginTop', true) + (options.offsetTop * 1);
}
} else {
offsetTop = options.offsetTop * 1;
}
}
if (options.offsetBottom) {
if (options.offsetParent && options.offsetBottom.match(/^[-+]\d+$/)) {
// add 1 pixel due to rounding problems...
offsetBottom = getScrollHeight() - (dimensions.offset(parent[0]).top + dimensions.height(parent[0])) + (options.offsetBottom * 1) + 1;
} else {
offsetBottom = options.offsetBottom * 1;
}
}
// Bring back the element's position after calculations
if (options.inlineStyles) {
element.css('position', initialPosition);
element.css('top', initialTop);
}
};
// Private methods
function getRequiredAffixClass (_unpin, position, elementHeight) {
var scrollTop = getScrollTop();
var scrollHeight = getScrollHeight();
if (scrollTop <= offsetTop) {
return 'top';
} else if (_unpin !== null) {
return scrollTop + _unpin <= position.top ? 'middle' : 'bottom';
} else if (offsetBottom !== null && (position.top + elementHeight + initialAffixTop >= scrollHeight - offsetBottom)) {
return 'bottom';
}
return 'middle';
}
function getScrollTop () {
return targetEl[0] === $window ? $window.pageYOffset : targetEl[0].scrollTop;
}
function getScrollHeight () {
return targetEl[0] === $window ? $window.document.body.scrollHeight : targetEl[0].scrollHeight;
}
$affix.init();
return $affix;
}
return AffixFactory;
};
})
.directive('bsAffix', function ($affix, $window, $timeout) {
return {
restrict: 'EAC',
require: '^?bsAffixTarget',
link: function postLink (scope, element, attr, affixTarget) {
var options = {scope: scope, target: affixTarget ? affixTarget.$element : angular.element($window)};
angular.forEach(['offsetTop', 'offsetBottom', 'offsetParent', 'offsetUnpin', 'inlineStyles', 'setWidth'], function (key) {
if (angular.isDefined(attr[key])) {
var option = attr[key];
if (/true/i.test(option)) option = true;
if (/false/i.test(option)) option = false;
options[key] = option;
}
});
var affix;
$timeout(function () { affix = $affix(element, options); });
scope.$on('$destroy', function () {
if (affix) affix.destroy();
options = null;
affix = null;
});
}
};
})
.directive('bsAffixTarget', function () {
return {
controller: function ($element) {
this.$element = $element;
}
};
});