Skip to content

Instantly share code, notes, and snippets.

@stuross
Created January 20, 2012 21:35
Show Gist options
  • Save stuross/1649731 to your computer and use it in GitHub Desktop.
Save stuross/1649731 to your computer and use it in GitHub Desktop.
hack to fill gaps in masonry
/**
* jQuery Masonry v2.1.01
* A dynamic layout plugin for jQuery
* The flip-side of CSS Floats
* http://masonry.desandro.com
*
* Licensed under the MIT license.
* Copyright 2011 David DeSandro
*/
(function( window, $, undefined ){
/*
* smartresize: debounced resize event for jQuery
*
* latest version and complete README available on Github:
* https://github.com/louisremi/jquery.smartresize.js
*
* Copyright 2011 @louis_remi
* Licensed under the MIT license.
*/
var $event = $.event,
resizeTimeout;
$event.special.smartresize = {
setup: function() {
$(this).bind( "resize", $event.special.smartresize.handler );
},
teardown: function() {
$(this).unbind( "resize", $event.special.smartresize.handler );
},
handler: function( event, execAsap ) {
// Save the context
var context = this,
args = arguments;
// set correct event type
event.type = "smartresize";
if ( resizeTimeout ) { clearTimeout( resizeTimeout ); }
resizeTimeout = setTimeout(function() {
jQuery.event.handle.apply( context, args );
}, execAsap === "execAsap"? 0 : 100 );
}
};
$.fn.smartresize = function( fn ) {
return fn ? this.bind( "smartresize", fn ) : this.trigger( "smartresize", ["execAsap"] );
};
// ========================= Masonry ===============================
// our "Widget" object constructor
$.Mason = function( options, element ){
this.element = $( element );
this._create( options );
this._init();
};
// styles of container element we want to keep track of
var masonryContainerStyles = [ 'position', 'height' ];
$.Mason.settings = {
isResizable: true,
isAnimated: false,
animationOptions: {
queue: false,
duration: 500
},
gutterWidth: 0,
isRTL: false,
isFitWidth: false
};
$.Mason.prototype = {
_filterFindBricks: function( $elems ) {
var selector = this.options.itemSelector;
// if there is a selector
// filter/find appropriate item elements
return !selector ? $elems : $elems.filter( selector ).add( $elems.find( selector ) );
},
_getBricks: function( $elems ) {
var $bricks = this._filterFindBricks( $elems )
.css({ position: 'absolute' })
.addClass('masonry-brick');
return $bricks;
},
// sets up widget
_create : function( options ) {
this.options = $.extend( true, {}, $.Mason.settings, options );
this.styleQueue = [];
// need to get bricks
this.reloadItems();
// get original styles in case we re-apply them in .destroy()
var elemStyle = this.element[0].style;
this.originalStyle = {};
for ( var i=0, len = masonryContainerStyles.length; i < len; i++ ) {
var prop = masonryContainerStyles[i];
this.originalStyle[ prop ] = elemStyle[ prop ] || '';
}
this.element.css({
position : 'relative'
});
this.horizontalDirection = this.options.isRTL ? 'right' : 'left';
this.offset = {
x: parseInt( this.element.css( 'padding-' + this.horizontalDirection ), 10 ),
y: parseInt( this.element.css( 'padding-top' ), 10 )
};
this.isFluid = this.options.columnWidth && typeof this.options.columnWidth === 'function';
// add masonry class first time around
var instance = this;
setTimeout( function() {
instance.element.addClass('masonry');
}, 0 );
// bind resize method
if ( this.options.isResizable ) {
$(window).bind( 'smartresize.masonry', function() {
instance.resize();
});
}
},
// _init fires when instance is first created
// and when instance is triggered again -> $el.masonry();
_init : function( callback ) {
this._getColumns();
this._reLayout( callback );
},
option: function( key, value ){
// set options AFTER initialization:
// signature: $('#foo').bar({ cool:false });
if ( $.isPlainObject( key ) ){
this.options = $.extend(true, this.options, key);
}
},
// ====================== General Layout ======================
// used on collection of atoms (should be filtered, and sorted before )
// accepts atoms-to-be-laid-out to start with
layout : function( $bricks, callback ) {
// place each brick
for (var i=0, len = $bricks.length; i < len; i++) {
this._placeBrick( $bricks[i] );
}
// set the size of the container
var containerSize = {};
containerSize.height = Math.max.apply( Math, this.colYs );
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;
containerSize.width = (this.cols - unusedCols) * this.columnWidth - this.options.gutterWidth;
}
this.styleQueue.push({ $el: this.element, style: containerSize });
// are we animating the layout arrangement?
// use plugin-ish syntax for css or animate
var styleFn = !this.isLaidOut ? 'css' : (
this.options.isAnimated ? 'animate' : 'css'
),
animOpts = this.options.animationOptions;
// process styleQueue
var obj;
for (i=0, len = this.styleQueue.length; i < len; i++) {
obj = this.styleQueue[i];
obj.$el[ styleFn ]( obj.style, animOpts );
}
// clear out queue for next time
this.styleQueue = [];
// provide $elems as context for the callback
if ( callback ) {
callback.call( $bricks );
}
this.isLaidOut = true;
},
// calculates number of columns
// i.e. this.columnWidth = 200
_getColumns : function() {
var container = this.options.isFitWidth ? this.element.parent() : this.element,
containerWidth = 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 ||
// or use the size of the first item
this.$bricks.outerWidth(true) ||
// 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 );
},
// layout logic
_placeBrick: function( brick ) {
var $brick = $(brick),
colSpan, groupCount, groupY, groupColY, j;
var filled_gap = false;
//how many columns does this brick span
colSpan = Math.ceil( $brick.outerWidth(true) /
( this.columnWidth + this.options.gutterWidth ) );
colSpan = Math.min( colSpan, this.cols );
if ( colSpan === 1 ) {
// if brick spans only one column, just like singleMode
groupY = [];
for(ucol in this.unused_colYs){
if(this.unused_colYs[ucol] != null){
var temp_unused = [];
for(uucol in this.unused_colYs[ucol]){
if(this.colYs[ucol] > this.unused_colYs[ucol][uucol]['y-position'] && $brick.outerHeight(true) <= this.unused_colYs[ucol][uucol]['height']){
if(!filled_gap){
groupY[ucol] = this.unused_colYs[ucol][uucol]['y-position'];
filled_gap = true;
if($brick.outerHeight(true) < this.unused_colYs[ucol][uucol]['height']){
temp_unused.push({'y-position': (this.unused_colYs[ucol][uucol]['y-position'] + $brick.outerHeight(true)), 'height': (this.unused_colYs[ucol][uucol]['height'] - $brick.outerHeight(true)) });
}
} else{
temp_unused.push(this.unused_colYs[ucol][uucol]);
}
} else{
temp_unused.push(this.unused_colYs[ucol][uucol]);
}
}
if(!filled_gap){
groupY[ucol] = this.colYs[ucol];
}
this.unused_colYs[ucol] = temp_unused;
} else{
groupY[ucol] = this.colYs[ucol];
}
}
//groupY = this.colYs;
} else {
// brick spans more than one column
// how many different places could this brick fit horizontally
groupCount = this.cols + 1 - colSpan;
groupY = [];
local_min = null;
// 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
groupY[j] = Math.max.apply( Math, groupColY );
local_group_min = Math.min.apply( Math, groupColY );
if(local_min == null || local_min > local_group_min){
local_min = local_group_min;
}
}
group_min = Math.min.apply( Math, groupY );
if(local_min < group_min){
for(ucol in groupY){
if(this.colYs[ucol] < groupY[ucol]){
this.unused_colYs[ucol].push({'y-position':this.colYs[ucol], 'height': (group_min - local_min)});
break;
}
}
}
}
// get the minimum Y value from the columns
var minimumY = Math.min.apply( Math, groupY ),
shortCol = 0;
// Find index of short column, the first from the left
for (var i=0, len = groupY.length; i < len; i++) {
if ( groupY[i] === minimumY ) {
shortCol = i;
break;
}
}
// position the brick
var position = {
top: minimumY + this.offset.y
};
// position.left or position.right
position[ this.horizontalDirection ] = this.columnWidth * shortCol + this.offset.x;
this.styleQueue.push({ $el: $brick, style: position });
// apply setHeight to necessary columns
if(!filled_gap){
var setHeight = minimumY + $brick.outerHeight(true),
setSpan = this.cols + 1 - len;
for ( i=0; i < setSpan; i++ ) {
this.colYs[ shortCol + i ] = setHeight;
}
}
},
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();
}
},
_reLayout : function( callback ) {
// reset columns
var i = this.cols;
this.colYs = [];
this.unused_colYs = []
while (i--) {
this.colYs.push( 0 );
this.unused_colYs.push( [] );
}
// apply layout logic to all bricks
this.layout( this.$bricks, callback );
},
// ====================== Convenience methods ======================
// goes through all children again and gets bricks in proper order
reloadItems : function() {
this.$bricks = this._getBricks( this.element.children() );
},
reload : function( callback ) {
this.reloadItems();
this._init( callback );
},
// convienence method for working with Infinite Scroll
appended : function( $content, isAnimatedFromBottom, callback ) {
if ( isAnimatedFromBottom ) {
// set new stuff to the bottom
this._filterFindBricks( $content ).css({ top: this.element.height() });
var instance = this;
setTimeout( function(){
instance._appended( $content, callback );
}, 1 );
} else {
this._appended( $content, callback );
}
},
_appended : function( $content, callback ) {
var $newBricks = this._getBricks( $content );
// add new bricks to brick pool
this.$bricks = this.$bricks.add( $newBricks );
this.layout( $newBricks, callback );
},
// removes elements from Masonry widget
remove : function( $content ) {
this.$bricks = this.$bricks.not( $content );
$content.remove();
},
// destroys widget, returns elements and container back (close) to original style
destroy : function() {
this.$bricks
.removeClass('masonry-brick')
.each(function(){
this.style.position = '';
this.style.top = '';
this.style.left = '';
});
// re-apply saved container styles
var elemStyle = this.element[0].style;
for ( var i=0, len = masonryContainerStyles.length; i < len; i++ ) {
var prop = masonryContainerStyles[i];
elemStyle[ prop ] = this.originalStyle[ prop ];
}
this.element
.unbind('.masonry')
.removeClass('masonry')
.removeData('masonry');
$(window).unbind('.masonry');
}
};
// ======================= imagesLoaded Plugin ===============================
/*!
* jQuery imagesLoaded plugin v1.1.0
* http://github.com/desandro/imagesloaded
*
* MIT License. by Paul Irish et al.
*/
// $('#my-container').imagesLoaded(myFunction)
// or
// $('img').imagesLoaded(myFunction)
// execute a callback when all images have loaded.
// needed because .load() doesn't work on cached images
// callback function gets image collection as argument
// `this` is the container
$.fn.imagesLoaded = function( callback ) {
var $this = this,
$images = $this.find('img').add( $this.filter('img') ),
len = $images.length,
blank = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==',
loaded = [];
function triggerCallback() {
callback.call( $this, $images );
}
function imgLoaded( event ) {
if ( event.target.src !== blank && $.inArray( this, loaded ) === -1 ){
loaded.push(this);
if ( --len <= 0 ){
setTimeout( triggerCallback );
$images.unbind( '.imagesLoaded', imgLoaded );
}
}
}
// if no images, trigger immediately
if ( !len ) {
triggerCallback();
}
$images.bind( 'load.imagesLoaded error.imagesLoaded', imgLoaded ).each( function() {
// cached images don't fire load sometimes, so we reset src.
var src = this.src;
// webkit hack from http://groups.google.com/group/jquery-dev/browse_thread/thread/eee6ab7b2da50e1f
// data uri bypasses webkit log warning (thx doug jones)
this.src = blank;
this.src = src;
});
return $this;
};
// helper function for logging errors
// $.error breaks jQuery chaining
var logError = function( message ) {
if ( this.console ) {
console.error( message );
}
};
// ======================= Plugin bridge ===============================
// leverages data method to either create or return $.Mason constructor
// A bit from jQuery UI
// https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.js
// A bit from jcarousel
// https://github.com/jsor/jcarousel/blob/master/lib/jquery.jcarousel.js
$.fn.masonry = function( options ) {
if ( typeof options === 'string' ) {
// call method
var args = Array.prototype.slice.call( arguments, 1 );
this.each(function(){
var instance = $.data( this, 'masonry' );
if ( !instance ) {
logError( "cannot call methods on masonry prior to initialization; " +
"attempted to call method '" + options + "'" );
return;
}
if ( !$.isFunction( instance[options] ) || options.charAt(0) === "_" ) {
logError( "no such method '" + options + "' for masonry instance" );
return;
}
// apply method
instance[ options ].apply( instance, args );
});
} else {
this.each(function() {
var instance = $.data( this, 'masonry' );
if ( instance ) {
// apply options & init
instance.option( options || {} );
instance._init();
} else {
// initialize new instance
$.data( this, 'masonry', new $.Mason( options, this ) );
}
});
}
return this;
};
})( window, jQuery );
@Morpho
Copy link

Morpho commented Nov 11, 2012

this it great! it helped me alot. this should be included in masonry!

@superdit
Copy link

superdit commented Jan 2, 2014

how to use this code??

@nightcoregirl
Copy link

This works great for first load, but responsive percentage width divs don't really scale anymore. what can I do about that? what do you suggest? is it better to use pixels width with this hack?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment