/*! * PerfectMasonry extension for Isotope * * Does similar things as the Isotopes "masonry" layoutmode, except that this one will actually go back and plug the holes * left by bigger elements, thus making a perfect brick wall. Profit! * * Usage: * $('#grid').isotope({ * layoutMode: 'perfectMasonry', * perfectMasonry: { * layout: 'horizontal', // Set layout as vertical/horizontal (default: vertical) * columnWidth: 200, // Set/Prefer columns to x wide (default: width of first tile) * rowHeight: 200, // Set/Prefer rows to y high (default: height of first tile) * * liquid: true, // Set layout as liquid (default: false) * cols: 3, // Force to have x columns (default: null) * rows: 3, // Force to have y rows (default: null) * minCols: 3, // Set min col count (default: 1) * minRows: 3, // Set min row count (default: 1) * maxCols: 5, // Set max col count (default: 9999) * maxRows: 4 // Set max row count (default: 9999) * } * }); * * * @author Mikko Tikkanen, Zonear Ltd. */ ;(function($, undefined) { var version = '1.2.1'; var isotope = null, $context = null, $container = null; $.extend($.Isotope.prototype, { isFirstRun: true, /** * Reset layout properties * * Runs before any layout change * -------------------------------------------------------------------------------------------------------- */ _perfectMasonryReset: function() { this.options.perfectMasonry = this.options.perfectMasonry|| {}; var isVertical = this.options.perfectMasonry.layout != 'horizontal', isLiquid = this.options.perfectMasonry.liquid == true; // Do things on a first run if(this.isFirstRun) { this.isFirstRun = false; isotope = this; $context = $(this.element.context); $container = $context.parent(); // Make sure we have min/maxCols & rows this.options.perfectMasonry.minCols = this.options.perfectMasonry.minCols || 1; this.options.perfectMasonry.minRows = this.options.perfectMasonry.minRows || 1; this.options.perfectMasonry.maxCols = this.options.perfectMasonry.maxCols || 9999; this.options.perfectMasonry.maxRows = this.options.perfectMasonry.maxRows || 9999; } // For liquid layout, replace default resize handler with forced relayout (outside firstRun as Isotope grid can be destroyed and created on the fly) if(isLiquid && $.data(window, 'events') && $._data(window, 'events').smartresize) { $(window).off('smartresize.isotope'); $(window).on('resize.isotope.perfectmasonry', function() { if(!$context.hasClass('isotope')) { return; } $context.isotope('reLayout'); }); } // Setup layout properties --------------------------------------------------- var properties = this.perfectMasonry = {}; // Fill properties with columnWidth and rowHeight (true argument) this._getSegments(); this._getSegments(true); // ...and with cols & rows this._perfectMasonryGetSegments(); // Handle liquid layout (cols, rows & sizes must be calculated on the fly) if(isLiquid) { var width = $container.width(), height = $container.height(); // Make sure we have colwidth & rowheight (get it from the calculated ) this.options.perfectMasonry.columnWidth = this.options.perfectMasonry.columnWidth || properties.columnWidth; this.options.perfectMasonry.rowHeight = this.options.perfectMasonry.rowHeight || properties.rowHeight; // Figure out how many cols & rows either have been set or can be fit into the container (also make sure we're still between min/max) properties.cols = this.options.perfectMasonry.cols || Math.floor(width / this.options.perfectMasonry.columnWidth); properties.rows = this.options.perfectMasonry.rows || Math.floor(height / this.options.perfectMasonry.rowHeight); properties.cols = Math.min(Math.max(properties.cols, this.options.perfectMasonry.minCols), this.options.perfectMasonry.maxCols); properties.rows = Math.min(Math.max(properties.rows, this.options.perfectMasonry.minRows), this.options.perfectMasonry.maxRows); // Recalculate accurate width/height so that the whole available space is used var diff = (isVertical ? properties.columnWidth / (width / properties.cols) : properties.rowHeight / (height / properties.rows)); properties.columnWidth = Math.floor(properties.columnWidth / diff); properties.rowHeight = Math.floor(properties.rowHeight / diff); } // Create top row of the grid properties.grid = new Array(this.perfectMasonry.cols); // Set container dimensions to 0 properties.containerHeight = 0; properties.containerWidth = 0; }, /** * Create layout * -------------------------------------------------------------------------------------------------------- */ _perfectMasonryLayout: function($elems) { var instance = this, properties = this.perfectMasonry, isVertical = instance.options.perfectMasonry.layout != 'horizontal', isLiquid = instance.options.perfectMasonry.liquid == true; // Create first set of the grid properties.grid = new Array(properties[(isVertical ? 'cols' : 'rows')]); if(!properties.grid || !properties.grid.length) { return; } // Loop each element $elems.each(function() { var $this = $(this); // Calculate col & row spans (with liquid layouts, store desired width as element data) var colSpan = (isLiquid ? $this.data('colSpan') : Math.ceil($this.outerWidth() / (properties.columnWidth + 1))), rowSpan = (isLiquid ? $this.data('rowSpan') : Math.ceil($this.outerHeight() / (properties.rowHeight + 1))); // For the first run with liquid layout, calculate sizes if(!colSpan) { colSpan = Math.ceil($this.outerWidth(true) / (properties.columnWidth + 1)); rowSpan = Math.ceil($this.outerHeight(true) / (properties.rowHeight + 1)); $this.data('colSpan', colSpan); $this.data('rowSpan', rowSpan); } /* Do the layout * -------------------------------------------------------------------------------- */ // Set spans var aSpan = (isVertical ? colSpan : rowSpan); var bSpan = (isVertical ? rowSpan : colSpan); // Bigger tiles can't fit into the last primary (though keep it still at least as 1) var max = Math.max((isVertical ? properties.cols - colSpan : properties.rows - rowSpan) + 1, 1); // Loop through/create primaries (set hard limit of 10.000 to prevent endless loop) var a = -1, x = 0, y = 0; while(++a < 10000) { properties.grid[a] = properties.grid[a] || []; // Go through the secondaries in the primary, set secondary and tile for (var b = 0; b < max; b++) { var tile = properties.grid[a][b]; // If the tile is not free, move to the next one immediately if(tile) { continue; } // Tiles spanning to multiple rows/columns - Check if it'll fit var doesFit = true; if(colSpan > 1 || rowSpan > 1) { for (var i = 0; i < aSpan; i++) { for (var j = 0; j < bSpan; j++) { properties.grid[a+j] = properties.grid[a+j] || []; if(properties.grid[a+j][b+i]) { doesFit = false; break; } } // If it doesn't fit, don't waste our time looping if(!doesFit) { break; } } } if(!doesFit) { continue } // Set all the cells as occupied for (var i = 0; i < aSpan; i++) { for (var j = 0; j < bSpan; j++) { properties.grid[a+j][b+i] = true; } } // Set x & y values var x = a, y = b; if(isVertical) { var x = b, y = a; } // Update container dimensions properties.containerWidth = Math.max(properties.containerWidth, (x + aSpan) * properties.columnWidth); properties.containerHeight = Math.max(properties.containerHeight, (y + bSpan) * properties.rowHeight); // In case of liquid layout, set element size full width/height if(instance.options.perfectMasonry.liquid == true) { $this.css({ width: properties.columnWidth * colSpan, height: properties.rowHeight * rowSpan }); } // Set the element location and GTFO instance._pushPosition($this, x*properties.columnWidth, y*properties.rowHeight); return; } } // If we got all the way down to here, the element doesn't fit - Hide it instance._pushPosition($this, -9999, -9999); }); // Set row & column count to container var rows = (isVertical ? properties.grid.length : properties.grid[0] && properties.grid[0].length), cols = (isVertical ? properties.grid[0] && properties.grid[0].length : properties.grid.length); $(this.element.context).attr('data-isotope-rows', rows).attr('data-isotope-cols', cols); }, /** * Get container size * * For resizing the container * -------------------------------------------------------------------------------------------------------- */ _perfectMasonryGetContainerSize: function() { return { width: this.perfectMasonry.containerWidth, height: this.perfectMasonry.containerHeight }; }, /** * Resize changed * * Figure out if layout changed * -------------------------------------------------------------------------------------------------------- */ _perfectMasonryResizeChanged: function() { var properties = this.perfectMasonry; // Store old values and calculate new numbers var oldCols = properties.cols, oldRows = properties.rows; this._perfectMasonryGetSegments(); // If new count was different, force layout change if(this.options.perfectMasonry.layout == 'horizontal' && oldRows !== properties.rows) { return true; } if(oldCols !== properties.cols) { return true; } return false; }, /** * Private * Do segment calculations by hand * -------------------------------------------------------------------------------------------------------- */ _perfectMasonryGetSegments: function() { var properties = this.perfectMasonry; var parent = this.options.perfectMasonry.parent || this.element.parent(); // Calculate columns var parentWidth = parent.width(); properties.cols = Math.floor(parentWidth / properties.columnWidth) || 1; // Calculate rows var parentHeight = parent.height(); properties.rows = Math.floor(parentHeight / properties.rowHeight) || 1; } }); })(jQuery);