mgcrea/angular-strap

View on GitHub
src/affix/affix.js

Summary

Maintainability
D
1 day
Test Coverage
'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;
      }
    };
  });