1/** 2 * Mosaic Flow 3 * 4 * Pinterest like responsive image grid that doesnât sucks 5 * 6 * @requires jQuery 7 * @author Artem Sapegin 8 * @copyright 2012 Artem Sapegin, http://sapegin.me 9 * @license MIT 10 */ 11 12/*jshint browser:true, jquery:true, white:false, smarttabs:true */ 13/*global jQuery:false, define:false*/ 14(function(factory) { // Try to register as an anonymous AMD module 15 if (typeof define === 'function' && define.amd) { 16 define(['jquery'], factory); 17 } 18 else { 19 factory(jQuery); 20 } 21}(function($) { 22 'use strict'; 23 var cnt = 0; 24 25 $.fn.mosaicflow = function(options) { 26 var args = Array.prototype.slice.call(arguments, 0); 27 28 return this.each(function() { 29 var elm = $(this); 30 var data = elm.data('mosaicflow'); 31 32 if (!data) { 33 options = $.extend({}, $.fn.mosaicflow.defaults, options, dataToOptions(elm)); 34 data = new Mosaicflow(elm, options); 35 elm.data('mosaicflow', data); 36 } 37 else if (typeof options === 'string') { 38 data[options](args[1]); 39 } 40 }); 41 }; 42 43 $.fn.mosaicflow.defaults = { 44 itemSelector: '> *', 45 columnClass: 'mosaicflow__column', 46 minItemWidth: 240, 47 minColumns: 2, 48 itemHeightCalculation: 'auto', 49 threshold: 40 50 }; 51 52 function Mosaicflow(container, options) { 53 this.container = container; 54 this.options = options; 55 56 this.container.trigger('mosaicflow-start'); 57 this.init(); 58 this.container.trigger('mosaicflow-ready'); 59 } 60 61 Mosaicflow.prototype = { 62 init: function() { 63 this.__uid = cnt++; 64 this.__uidItemCounter = 0; 65 this.items = this.container.find(this.options.itemSelector); 66 this.columns = $([]); 67 this.columnsHeights = []; 68 this.itemsHeights = {};
69 this.tempContainer = $('<div>').css({'visibility': 'hidden', 'width': '100%'}); 70 this.workOnTemp = false; 71 this.autoCalculation = this.options.itemHeightCalculation === 'auto'; 72 73 this.container.append(this.tempContainer); 74 75 var that = this; 76 this.items.each(function() { 77 var elm = $(this); 78 var id = elm.attr('id'); 79 if (!id) { 80 // Generate an unique id 81 id = that.generateUniqueId(); 82 elm.attr('id', id); 83 } 84 }); 85 86 this.container.css('visibility', 'hidden'); 87 if (this.autoCalculation) { 88 $(window).on('load', $.proxy(this.refill, this)); 89 } 90 else { 91 this.refill(); 92 } 93 $(window).resize($.proxy(this.refill, this)); 94 }, 95 96 refill: function() { 97 this.container.trigger('mosaicflow-fill'); 98 this.numberOfColumns = Math.floor(this.container.width() / this.options.minItemWidth); 99 // Always keep min columns number 100 if (this.numberOfColumns < this.options.minColumns) 101 this.numberOfColumns = this.options.minColumns; 102 103 var needToRefill = this.ensureColumns(); 104 if (needToRefill) { 105 this.fillColumns(); 106 107 // Remove excess columns, only if there are visible columns remaining 108 if (this.columns.filter(':visible').length > 0) { 109 this.columns.filter(':hidden').remove(); 110 } 111 } 112 this.container.css('visibility', 'visible'); 113 this.container.trigger('mosaicflow-filled'); 114 }, 115 116 ensureColumns: function() { 117 var createdCnt = this.columns.filter(':visible').length; 118 var calculatedCnt = this.numberOfColumns; 119 120 this.workingContainer = createdCnt === 0 ? this.tempContainer : this.container; 121 122 if (calculatedCnt > createdCnt) { 123 var neededCnt = calculatedCnt - createdCnt; 124 for (var columnIdx = 0; columnIdx < neededCnt; columnIdx++) { 125 var column = $('<div>', { 126 'class': this.options.columnClass 127 }); 128 129 this.workingContainer.append(column); 130 } 131 } 132 else if (calculatedCnt < createdCnt) { 133 var lastColumn = createdCnt; 134 while (calculatedCnt <= lastColumn) { 135 // We can't remove columns here becase it will remove items to. So we hide it and will remove later. 136 this.columns.eq(lastColumn).hide(); 137 lastColumn--; 138 } 139 140 var diff = createdCnt - calculatedCnt; 141 this.columnsHeights.splice(this.columnsHeights.length - diff, diff); 142 } 143 144 if (calculatedCnt !== createdCnt) { 145 this.columns = this.workingContainer.find('.' + this.options.columnClass); 146 this.columns.css('width', (100 / calculatedCnt) + '%'); 147 return true; 148 } 149 150 return false; 151 }, 152 153 fillColumns: function() { 154 var columnsCnt = this.numberOfColumns; 155 var itemsCnt = this.items.length; 156 157 for (var columnIdx = 0; columnIdx < columnsCnt; columnIdx++) { 158 var column = this.columns.eq(columnIdx); 159 this.columnsHeights[columnIdx] = 0; 160 for (var itemIdx = columnIdx; itemIdx < itemsCnt; itemIdx += columnsCnt) { 161 var item = this.items.eq(itemIdx); 162 var height = 0; 163 column.append(item); 164 165 if (this.autoCalculation) { 166 // Check height after being placed in its column 167 height = item.outerHeight(); 168 } 169 else { 170 // Read img height attribute 171 height = parseInt(item.find('img').attr('height'), 10); 172 } 173 174 this.itemsHeights[item.attr('id')] = height; 175 this.columnsHeights[columnIdx] += height; 176 } 177 } 178 179 this.levelBottomEdge(this.itemsHeights, this.columnsHeights); 180 181 if (this.workingContainer === this.tempContainer) { 182 this.container.append(this.tempContainer.children()); 183 } 184 this.container.trigger('mosaicflow-layout'); 185 }, 186 187 levelBottomEdge: function(itemsHeights, columnsHeights) { 188 while (true) { 189 var lowestColumn = $.inArray(Math.min.apply(null, columnsHeights), columnsHeights); 190 var highestColumn = $.inArray(Math.max.apply(null, columnsHeights), columnsHeights); 191 if (lowestColumn === highestColumn) return; 192 193 var lastInHighestColumn = this.columns.eq(highestColumn).children().last(); 194 var lastInHighestColumnHeight = itemsHeights[lastInHighestColumn.attr('id')]; 195 var lowestHeight = columnsHeights[lowestColumn]; 196 var highestHeight = columnsHeights[highestColumn]; 197 var newLowestHeight = lowestHeight + lastInHighestColumnHeight; 198 199 if (newLowestHeight >= highestHeight) return; 200 201 if (highestHeight - newLowestHeight < this.options.threshold) return; 202
203 this.columns.eq(lowestColumn).append(lastInHighestColumn); 204 columnsHeights[highestColumn] -= lastInHighestColumnHeight; 205 columnsHeights[lowestColumn] += lastInHighestColumnHeight; 206 } 207 }, 208 209 add: function(elm) { 210 this.container.trigger('mosaicflow-item-add', [elm]); 211 var lowestColumn = $.inArray(Math.min.apply(null, this.columnsHeights), this.columnsHeights); 212 var height = 0; 213 214 if (this.autoCalculation) { 215 216 // Get height of elm 217 elm.css({ 218 position: 'static', 219 visibility: 'hidden', 220 display: 'block' 221 }).appendTo(this.columns.eq(lowestColumn)); 222 223 height = elm.outerHeight(); 224 225 var inlineImages = elm.find('img'); 226 if (inlineImages.length !== 0) { 227 inlineImages.each(function() { 228 var image = $(this); 229 var imageSizes = getImageSizes(image); 230 var actualHeight = (image.width() * imageSizes.height) / imageSizes.width; 231 232 height += actualHeight; 233 }); 234 } 235 236 elm.detach().css({ 237 position: 'static', 238 visibility: 'visible' 239 }); 240 } 241 else { 242 height = parseInt(elm.find('img').attr('height'), 10); 243 } 244 245 if (!elm.attr('id')) { 246 // Generate a unique id 247 elm.attr('id', this.generateUniqueId()); 248 } 249 250 // Update item collection. 251 // Item needs to be placed at the end of this.items to keep order of elements 252 var itemsArr = this.items.toArray(); 253 itemsArr.push(elm); 254 this.items = $(itemsArr); 255 256 this.itemsHeights[elm.attr('id')] = height; 257 this.columnsHeights[lowestColumn] += height; 258 this.columns.eq(lowestColumn).append(elm); 259 260 this.levelBottomEdge(this.itemsHeights, this.columnsHeights); 261 this.container.trigger('mosaicflow-layout'); 262 this.container.trigger('mosaicflow-item-added', [elm]); 263 }, 264 265 remove: function(elm) { 266 this.container.trigger('mosaicflow-item-remove', [elm]); 267 var column = elm.parents('.' + this.options.columnClass); 268 269 // Update column height 270 this.columnsHeights[column.index() - 1] -= this.itemsHeights[elm.attr('id')]; 271 272 elm.detach(); 273 274 // Update item collection 275 this.items = this.items.not(elm); 276 this.levelBottomEdge(this.itemsHeights, this.columnsHeights); 277 this.container.trigger('mosaicflow-layout'); 278 this.container.trigger('mosaicflow-item-removed', [elm]); 279 }, 280 281 empty: function() { 282 var columnsCnt = this.numberOfColumns; 283 284 this.items = $([]); 285 this.itemsHeights = {}; 286 287 for (var columnIdx = 0; columnIdx < columnsCnt; columnIdx++) { 288 var column = this.columns.eq(columnIdx); 289 this.columnsHeights[columnIdx] = 0; 290 column.empty(); 291 } 292 this.container.trigger('mosaicflow-layout'); 293 }, 294 295 recomputeHeights: function() { 296 function computeHeight(idx, item) { 297 item = $(item); 298 var height = 0; 299 if (that.autoCalculation) { 300 // Check height after being placed in its column 301 height = item.outerHeight(); 302 } 303 else { 304 // Read img height attribute 305 height = parseInt(item.find('img').attr('height'), 10); 306 } 307 308 that.itemsHeights[item.attr('id')] = height; 309 that.columnsHeights[columnIdx] += height; 310 } 311 312 var that = this; 313 var columnsCnt = this.numberOfColumns; 314 315 for (var columnIdx = 0; columnIdx < columnsCnt; columnIdx++) { 316 var column = this.columns.eq(columnIdx); 317 318 this.columnsHeights[columnIdx] = 0; 319 column.children().each(computeHeight); 320 } 321 }, 322 323 generateUniqueId: function() { 324 // Increment the counter 325 this.__uidItemCounter++; 326 327 // Return an unique ID 328 return 'mosaic-' + this.__uid + '-itemid-' + this.__uidItemCounter; 329 } 330 }; 331 332 // Camelize data-attributes 333 function dataToOptions(elem) { 334 function upper(m, l) { 335 return l.toUpper(); 336 } 337 var options = {}; 338 var data = elem.data(); 339 for (var key in data) { 340 options[key.replace(/-(\w)/g, upper)] = data[key]; 341 } 342 return options; 343 } 344 345 function getImageSizes(image) { 346 var sizes = {}; 347 348 sizes.height = parseInt(image.attr('height'), 10); 349 sizes.width = parseInt(image.attr('width'), 10); 350 351 if (sizes.height === 0 || sizes.width === 0) { 352 var utilImage = new Image(); 353 utilImage.src = image.attr('src'); 354 355 sizes.width = utilImage.width; 356 sizes.height = utilImage.height; 357 } 358 359 return sizes; 360 } 361 362 // Auto init 363 $(function() { $('.mosaicflow').mosaicflow(); }); 364 365}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.