balsamine.2013-2014
clone your own copy | download snapshot

Snapshots | iceberg

Inside this repository

masonry.js
application/javascript

Download raw (14.7 KB)

/**
 * Vanilla Masonry v1.0.7
 * Dynamic layouts for the flip-side of CSS Floats
 * http://vanilla-masonry.desandro.com
 *
 * Licensed under the MIT license.
 * Copyright 2012 David DeSandro
 */

(function( window, undefined ) {

  'use strict';

  var document = window.document;

  // -------------------------- DOM Utility -------------------------- //

  // from bonzo.js, by Dustin Diaz - https://github.com/ded/bonzo

  // use classList API if available
  var supportClassList = 'classList' in document.createElement('div');

  function classReg(c) {
    return new RegExp("(^|\\s+)" + c + "(\\s+|$)");
  }

  var hasClass = supportClassList ? function (el, c) {
    return el.classList.contains(c);
  } : function (el, c) {
    return classReg(c).test(el.className);
  };

  var addClass = supportClassList ? function (el, c) {
    el.classList.add(c);
  } : function (el, c) {
    if ( !hasClass(el, c) ) {
      el.className = el.className + ' ' + c;
    }
  };

  var removeClass = supportClassList ? function (el, c) {
    el.classList.remove(c);
  } : function (el, c) {
    el.className = el.className.replace(classReg(c), ' ');
  };

  // -------------------------- getStyle -------------------------- //

  var defView = document.defaultView;

  var getStyle = defView && defView.getComputedStyle ?
    function( elem ) {
      return defView.getComputedStyle( elem, null );
    } :
    function( elem ) {
      return elem.currentStyle;
    };

  // -------------------------- Percent Margin support -------------------------- //

  // hack for WebKit bug, which does not return proper values for percent-margins
  // Hard work done by Mike Sherov https://github.com/jquery/jquery/pull/616

  var body = document.getElementsByTagName("body")[0],
      div = document.createElement('div'),
      fakeBody = body || document.createElement('body');

  div.style.marginTop = '1%';
  fakeBody.appendChild( div );

  var supportsPercentMargin = getStyle( div ).marginTop !== '1%';

  fakeBody.removeChild( div );

  // TODO remove fakebody if it's fake?

  // https://github.com/mikesherov/jquery/blob/191c9c1be/src/css.js

  function hackPercentMargin( elem, computedStyle, marginValue ) {
    if ( marginValue.indexOf('%') === -1 ) {
      return marginValue;
    }

    var elemStyle = elem.style,
        originalWidth = elemStyle.width,
        ret;

    // get measure by setting it on elem's width
    elemStyle.width = marginValue;
    ret = computedStyle.width;
    elemStyle.width = originalWidth;

    return ret;
  }

  // -------------------------- getWH -------------------------- //

  // returns width/height of element, refactored getWH from jQuery
  function getWH( elem, measure, isOuter ) {
    // Start with offset property
    var isWidth = measure !== 'height',
        val = isWidth ? elem.offsetWidth : elem.offsetHeight,
        dirA = isWidth ? 'Left' : 'Top',
        dirB = isWidth ? 'Right' : 'Bottom',
        computedStyle = getStyle( elem ),
        paddingA = parseFloat( computedStyle[ 'padding' + dirA ] ) || 0,
        paddingB = parseFloat( computedStyle[ 'padding' + dirB ] ) || 0,
        borderA = parseFloat( computedStyle[ 'border' + dirA + 'Width' ] ) || 0,
        borderB = parseFloat( computedStyle[ 'border' + dirB + 'Width' ] ) || 0,
        computedMarginA = computedStyle[ 'margin' + dirA ],
        computedMarginB = computedStyle[ 'margin' + dirB ],
        marginA, marginB;

    if ( !supportsPercentMargin ) {
      computedMarginA = hackPercentMargin( elem, computedStyle, computedMarginA );
      computedMarginB = hackPercentMargin( elem, computedStyle, computedMarginB );
    }

    marginA = parseFloat( computedMarginA ) || 0;
    marginB = parseFloat( computedMarginB ) || 0;

    if ( val > 0 ) {

      if ( isOuter ) {
        // outerWidth, outerHeight, add margin
        val += marginA + marginB;
      } else {
        // like getting width() or height(), no padding or border
        val -= paddingA + paddingB + borderA + borderB;
      }

    } else {

      // Fall back to computed then uncomputed css if necessary
      val = computedStyle[ measure ];
      if ( val < 0 || val === null ) {
        val = elem.style[ measure ] || 0;
      }
      // Normalize "", auto, and prepare for extra
      val = parseFloat( val ) || 0;

      if ( isOuter ) {
        // Add padding, border, margin
        val += paddingA + paddingB + marginA + marginB + borderA + borderB;
      }
    }

    return val;
  }

  // -------------------------- addEvent / removeEvent -------------------------- //

  // by John Resig - http://ejohn.org/projects/flexible-javascript-events/

  function addEvent( obj, type, fn ) {
    if ( obj.addEventListener ) {
      obj.addEventListener( type, fn, false );
    } else if ( obj.attachEvent ) {
      obj[ 'e' + type + fn ] = fn;
      obj[ type + fn ] = function() {
        obj[ 'e' + type + fn ]( window.event );
      };
      obj.attachEvent( "on" + type, obj[ type + fn ] );
    }
  }

  function removeEvent( obj, type, fn ) {
    if ( obj.removeEventListener ) {
      obj.removeEventListener( type, fn, false );
    } else if ( obj.detachEvent ) {
      obj.detachEvent( "on" + type, obj[ type + fn ] );
      obj[ type + fn ] = null;
      obj[ 'e' + type + fn ] = null;
    }
  }

  // -------------------------- Masonry -------------------------- //

  function Masonry( elem, options ) {
    if ( !elem ) {
      // console.error('Element not found for Masonry.')
      return;
    }

    this.element = elem;

    this.options = {};

    for ( var prop in Masonry.defaults ) {
      this.options[ prop ] = Masonry.defaults[ prop ];
    }

    for ( prop in options ) {
      this.options[ prop ] = options[ prop ];
    }

    this._create();
    this.build();
  }

  // styles of container element we want to keep track of
  var masonryContainerStyles = [ 'position', 'height' ];

  Masonry.defaults = {
    isResizable: true,
    gutterWidth: 0,
    isRTL: false,
    isFitWidth: false
  };

  Masonry.prototype = {

    _getBricks: function( items ) {
      var item;
      for (var i=0, len = items.length; i < len; i++ ) {
        item = items[i];
        item.style.position = 'absolute';
        addClass( item, 'masonry-brick' );
        this.bricks.push( item );
      }
    },

    _create: function() {

      // need to get bricks
      this.reloadItems();

      // get original styles in case we re-apply them in .destroy()
      var elemStyle = this.element.style;
      this._originalStyle = {};
      for ( var i=0, len = masonryContainerStyles.length; i < len; i++ ) {
        var prop = masonryContainerStyles[i];
        this._originalStyle[ prop ] = elemStyle[ prop ] || '';
      }

      this.element.style.position = 'relative';

      this.horizontalDirection = this.options.isRTL ? 'right' : 'left';
      this.offset = {};

      // get top left/right position of where the bricks should be
      var computedStyle = getStyle( this.element ),
          paddingX = this.options.isRTL ? 'paddingRight' : 'paddingLeft';

      this.offset.y = parseFloat( computedStyle.paddingTop ) || 0;
      this.offset.x = parseFloat( computedStyle[ paddingX ] ) || 0;

      this.isFluid = this.options.columnWidth && typeof this.options.columnWidth === 'function';

      // add masonry class first time around
      var instance = this;
      setTimeout( function() {
        addClass( instance.element, 'masonry' );
      });

      // bind resize method
      if ( this.options.isResizable ) {
        addEvent( window, 'resize', function(){
          instance._handleResize();
        });
      }

    },

    // build fires when instance is first created
    // and when instance is triggered again -> myMasonry.build();
    build: function( callback ) {
      this._getColumns();
      this._reLayout( callback );
    },

    // calculates number of columns
    // i.e. this.columnWidth = 200
    _getColumns: function() {
      var container = this.options.isFitWidth ? this.element.parentNode : this.element,
          containerWidth = getWH( container, 'width' );

                         // use fluid columnWidth function if there
      this.columnWidth = this.isFluid ? this.options.columnWidth( containerWidth ) :
                    // if not, how about the explicitly set option?
                    this.options.columnWidth ||
                    // Okay then, use the size of the first item
                     getWH( this.bricks[0], 'width', true ) ||
                    // Whatevs, if there's no items, use size of container
                    containerWidth;

      this.columnWidth += this.options.gutterWidth;

      this.cols = Math.floor( ( containerWidth + this.options.gutterWidth ) / this.columnWidth );
      this.cols = Math.max( this.cols, 1 );

    },

    // goes through all children again and gets bricks in proper order
    reloadItems: function() {
      this.bricks = [];
      this._getBricks( this.element.children );
    },

    // ====================== General Layout ======================

    _reLayout: function( callback ) {
      // reset columns
      var i = this.cols;
      this.colYs = [];
      while (i--) {
        this.colYs.push( 0 );
      }
      // apply layout logic to all bricks
      this.layout( this.bricks, callback );
    },

    // used on collection of atoms (should be filtered, and sorted before )
    // accepts bricks-to-be-laid-out to start with
    layout: function( bricks, callback ) {

      // bail out if no bricks
      if ( !bricks || !bricks.length ) {
        return;
      }

      // layout logic
      var brick, colSpan, groupCount, groupY, groupColY, j, colGroup;

      for ( var i=0, len = bricks.length; i < len; i++ ) {
        brick = bricks[i];

        // don't try nothing on text, imlookinachu IE6-8
        if ( brick.nodeType !== 1 ) {
          continue;
        }

        //how many columns does this brick span
        colSpan = Math.ceil( getWH( brick, 'width', true ) / this.columnWidth );
        colSpan = Math.min( colSpan, this.cols );

        if ( colSpan === 1 ) {
          // if brick spans only one column, just like singleMode
          colGroup = this.colYs;
        } else {
          // brick spans more than one column
          // how many different places could this brick fit horizontally
          groupCount = this.cols + 1 - colSpan;
          colGroup = [];

          // for each group potential horizontal position
          for ( j=0; j < groupCount; j++ ) {
            // make an array of colY values for that one group
            groupColY = this.colYs.slice( j, j + colSpan );
            // and get the max value of the array
            colGroup[j] = Math.max.apply( Math, groupColY );
          }

        }

        // get the minimum Y value from the columns
        var minimumY = Math.min.apply( Math, colGroup );

        // Find index of short column, the first from the left
        for ( var colI = 0, groupLen = colGroup.length; colI < groupLen; colI++ ) {
          if ( colGroup[ colI ] === minimumY ) {
            break;
          }
        }

        // position the brick
        brick.style.top = ( minimumY + this.offset.y ) + 'px';
        brick.style[ this.horizontalDirection ] = ( this.columnWidth * colI + this.offset.x ) + 'px';

        // apply setHeight to necessary columns
        var setHeight = minimumY + getWH( brick, 'height', true ),
            setSpan = this.cols + 1 - groupLen;
        for ( j=0; j < setSpan; j++ ) {
          this.colYs[ colI + j ] = setHeight;
        }

      }

      // set the size of the container
      var containerWidth = {};
      this.element.style.height = Math.max.apply( Math, this.colYs ) + 'px';
      if ( this.options.isFitWidth ) {
        var unusedCols = 0;
        i = this.cols;
        // count unused columns
        while ( --i ) {
          if ( this.colYs[i] !== 0 ) {
            break;
          }
          unusedCols++;
        }
        // fit container to columns that have been used;
        this.element.style.width = ( (this.cols - unusedCols) * this.columnWidth -
          this.options.gutterWidth ) + 'px';
      }

      // provide bricks as context for the callback
      if ( callback ) {
        callback.call( bricks );
      }

    },

    // ====================== resize ======================

    // original debounce by John Hann
    // http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/

    // this fires every resize
    _handleResize: function() {
      var instance = this;

      function delayed() {
        instance.resize();
        instance._resizeTimeout = null;
      }

      if ( this._resizeTimeout ) {
        clearTimeout( this._resizeTimeout );
      }

      this._resizeTimeout = setTimeout( delayed, 100 );
    },

    // debounced
    resize: function() {
      var prevColCount = this.cols;
      // get updated colCount
      this._getColumns();
      if ( this.isFluid || this.cols !== prevColCount ) {
        // if column count has changed, trigger new layout
        this._reLayout();
      }
    },

    // ====================== methods ======================

    // for prepending
    reload: function( callback ) {
      this.reloadItems();
      this.build( callback );
    },

    // convienence method for working with Infinite Scroll
    appended: function( items, isAnimatedFromBottom, callback ) {
      var instance = this,
          layoutAppendedItems = function() {
            instance._appended( items, callback );
          };

      if ( isAnimatedFromBottom ) {
        // set new stuff to the bottom
        var y = getWH( this.element, 'height' ) + 'px';
        for (var i=0, len = items.length; i < len; i++) {
          items[i].style.top = y;
        }
        // layout items async after initial height has been set
        setTimeout( layoutAppendedItems, 1 );
      } else {
        layoutAppendedItems();
      }
    },

    _appended: function( items, callback ) {
      // add new bricks to brick pool
      this._getBricks( items );
      this.layout( items, callback );
    },

    destroy: function() {
      var brick;
      for (var i=0, len = this.bricks.length; i < len; i++) {
        brick = this.bricks[i];
        brick.style.position = '';
        brick.style.top = '';
        brick.style.left = '';
        removeClass( brick, 'masonry-brick' );
      }

      // re-apply saved container styles
      var elemStyle = this.element.style;
      len = masonryContainerStyles.length;
      for ( i=0; i < len; i++ ) {
        var prop = masonryContainerStyles[i];
        elemStyle[ prop ] = this._originalStyle[ prop ];
      }

      removeClass( this.element, 'masonry' );

      if ( this.resizeHandler ) {
        removeEvent( window, 'resize', this.resizeHandler );
      }

    }

  };

  // add utility function
  Masonry.getWH = getWH;
  // add Masonry to global namespace
  window.Masonry = Masonry;

})( window );