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: 1, 48 itemHeightCalculation: 'auto', 49 threshold: 40, 50 lg:5, 51 md:4, 52 sm:2, 53 xs:1 54 }; 55 56 function Mosaicflow(container, options) { 57 this.container = container; 58 this.options = options; 59 60 this.container.trigger('mosaicflow-start'); 61 this.init(); 62 this.container.trigger('mosaicflow-ready'); 63 } 64 65 Mosaicflow.prototype = { 66 init: function() { 67 this.__uid = cnt++; 68 this.__uidItemCounter = 0; 69 this.items = this.container.find(this.options.itemSelector); 70 this.columns = $([]); 71 this.columnsHeights = []; 72 this.itemsHeights = {};
73 this.tempContainer = $('<div>').css({'visibility': 'hidden', 'width': '100%'}); 74 this.workOnTemp = false; 75 this.autoCalculation = this.options.itemHeightCalculation === 'auto'; 76 77 this.container.append(this.tempContainer); 78 79 var that = this; 80 this.items.each(function() { 81 var elm = $(this); 82 var id = elm.attr('id'); 83 if (!id) { 84 // Generate an unique id 85 id = that.generateUniqueId(); 86 elm.attr('id', id); 87 } 88 }); 89 90 this.container.css('visibility', 'hidden'); 91 if (this.autoCalculation) { 92 $(window).on('load', $.proxy(this.refill, this)); 93 } 94 else { 95 this.refill(); 96 } 97 $(window).resize($.proxy(this.refill, this)); 98 }, 99 100 refill: function() { 101 this.container.trigger('mosaicflow-fill'); 102 //this.numberOfColumns = Math.floor(this.container.width() / this.options.minItemWidth); 103 104 if($( document ).width() >= 1200) 105 { 106 this.numberOfColumns = this.options.lg; 107 } else { 108 if($( document ).width() >= 992) 109 { 110 this.numberOfColumns = this.options.md; 111 } else { 112 if($( document ).width() >= 768) 113 { 114 this.numberOfColumns = this.options.sm; 115 } else { 116 this.numberOfColumns = this.options.xs; 117 } 118 } 119 } 120 121 // Always keep min columns number 122 if (this.numberOfColumns < this.options.minColumns) 123 this.numberOfColumns = this.options.minColumns; 124 125 var needToRefill = this.ensureColumns(); 126 if (needToRefill) { 127 this.fillColumns(); 128 129 // Remove excess columns, only if there are visible columns remaining 130 if (this.columns.filter(':visible').length > 0) { 131 this.columns.filter(':hidden').remove(); 132 } 133 } 134 this.container.css('visibility', 'visible'); 135 this.container.trigger('mosaicflow-filled'); 136 }, 137 138 ensureColumns: function() { 139 var createdCnt = this.columns.filter(':visible').length; 140 var calculatedCnt = this.numberOfColumns; 141 142 this.workingContainer = createdCnt === 0 ? this.tempContainer : this.container; 143 144 if (calculatedCnt > createdCnt) { 145 var neededCnt = calculatedCnt - createdCnt; 146 for (var columnIdx = 0; columnIdx < neededCnt; columnIdx++) { 147 var column = $('<div>', { 148 'class': this.options.columnClass 149 }); 150 151 this.workingContainer.append(column); 152 } 153 } 154 else if (calculatedCnt < createdCnt) { 155 var lastColumn = createdCnt; 156 while (calculatedCnt <= lastColumn) { 157 // We can't remove columns here becase it will remove items to. So we hide it and will remove later. 158 this.columns.eq(lastColumn).hide(); 159 lastColumn--; 160 } 161 162 var diff = createdCnt - calculatedCnt; 163 this.columnsHeights.splice(this.columnsHeights.length - diff, diff); 164 } 165 166 if (calculatedCnt !== createdCnt) { 167 this.columns = this.workingContainer.find('.' + this.options.columnClass); 168 this.columns.css('width', (100 / calculatedCnt) + '%'); 169 return true; 170 } 171 172 return false; 173 }, 174 175 fillColumns: function() { 176 var columnsCnt = this.numberOfColumns; 177 var itemsCnt = this.items.length; 178 179 for (var columnIdx = 0; columnIdx < columnsCnt; columnIdx++) { 180 var column = this.columns.eq(columnIdx); 181 this.columnsHeights[columnIdx] = 0; 182 for (var itemIdx = columnIdx; itemIdx < itemsCnt; itemIdx += columnsCnt) { 183 var item = this.items.eq(itemIdx); 184 var height = 0; 185 column.append(item); 186 187 if (this.autoCalculation) { 188 // Check height after being placed in its column 189 height = item.outerHeight(); 190 } 191 else { 192 // Read img height attribute 193 height = parseInt(item.find('img').attr('height'), 10); 194 } 195 196 this.itemsHeights[item.attr('id')] = height; 197 this.columnsHeights[columnIdx] += height; 198 } 199 } 200 201 this.levelBottomEdge(this.itemsHeights, this.columnsHeights); 202 203 if (this.workingContainer === this.tempContainer) { 204 this.container.append(this.tempContainer.children()); 205 } 206 this.container.trigger('mosaicflow-layout'); 207 }, 208 209 levelBottomEdge: function(itemsHeights, columnsHeights) { 210 while (true) { 211 var lowestColumn = $.inArray(Math.min.apply(null, columnsHeights), columnsHeights); 212 var highestColumn = $.inArray(Math.max.apply(null, columnsHeights), columnsHeights); 213 if (lowestColumn === highestColumn) return; 214
215 var lastInHighestColumn = this.columns.eq(highestColumn).children().last(); 216 var lastInHighestColumnHeight = itemsHeights[lastInHighestColumn.attr('id')]; 217 var lowestHeight = columnsHeights[lowestColumn]; 218 var highestHeight = columnsHeights[highestColumn]; 219 var newLowestHeight = lowestHeight + lastInHighestColumnHeight; 220 221 if (newLowestHeight >= highestHeight) return; 222 223 if (highestHeight - newLowestHeight < this.options.threshold) return; 224 225 this.columns.eq(lowestColumn).append(lastInHighestColumn); 226 columnsHeights[highestColumn] -= lastInHighestColumnHeight; 227 columnsHeights[lowestColumn] += lastInHighestColumnHeight; 228 } 229 }, 230 231 add: function(elm) { 232 this.container.trigger('mosaicflow-item-add', [elm]); 233 var lowestColumn = $.inArray(Math.min.apply(null, this.columnsHeights), this.columnsHeights); 234 var height = 0; 235 236 if (this.autoCalculation) { 237 238 // Get height of elm 239 elm.css({ 240 position: 'static', 241 visibility: 'hidden', 242 display: 'block' 243 }).appendTo(this.columns.eq(lowestColumn)); 244 245 height = elm.outerHeight(); 246 247 var inlineImages = elm.find('img'); 248 if (inlineImages.length !== 0) { 249 inlineImages.each(function() { 250 var image = $(this); 251 var imageSizes = getImageSizes(image); 252 var actualHeight = (image.width() * imageSizes.height) / imageSizes.width; 253 254 height += actualHeight; 255 }); 256 } 257 258 elm.detach().css({ 259 position: 'static', 260 visibility: 'visible' 261 }); 262 } 263 else { 264 height = parseInt(elm.find('img').attr('height'), 10); 265 } 266 267 if (!elm.attr('id')) { 268 // Generate a unique id 269 elm.attr('id', this.generateUniqueId()); 270 } 271 272 // Update item collection. 273 // Item needs to be placed at the end of this.items to keep order of elements 274 var itemsArr = this.items.toArray(); 275 itemsArr.push(elm); 276 this.items = $(itemsArr); 277 278 this.itemsHeights[elm.attr('id')] = height; 279 this.columnsHeights[lowestColumn] += height; 280 this.columns.eq(lowestColumn).append(elm); 281 282 this.levelBottomEdge(this.itemsHeights, this.columnsHeights); 283 this.container.trigger('mosaicflow-layout'); 284 this.container.trigger('mosaicflow-item-added', [elm]); 285 }, 286 287 remove: function(elm) { 288 this.container.trigger('mosaicflow-item-remove', [elm]); 289 var column = elm.parents('.' + this.options.columnClass); 290 291 // Update column height 292 this.columnsHeights[column.index() - 1] -= this.itemsHeights[elm.attr('id')]; 293 294 elm.detach(); 295 296 // Update item collection 297 this.items = this.items.not(elm); 298 this.levelBottomEdge(this.itemsHeights, this.columnsHeights); 299 this.container.trigger('mosaicflow-layout'); 300 this.container.trigger('mosaicflow-item-removed', [elm]); 301 }, 302 303 empty: function() { 304 var columnsCnt = this.numberOfColumns; 305 306 this.items = $([]); 307 this.itemsHeights = {}; 308 309 for (var columnIdx = 0; columnIdx < columnsCnt; columnIdx++) { 310 var column = this.columns.eq(columnIdx); 311 this.columnsHeights[columnIdx] = 0; 312 column.empty(); 313 } 314 this.container.trigger('mosaicflow-layout'); 315 }, 316 317 recomputeHeights: function() { 318 function computeHeight(idx, item) { 319 item = $(item); 320 var height = 0; 321 if (that.autoCalculation) { 322 // Check height after being placed in its column 323 height = item.outerHeight(); 324 } 325 else { 326 // Read img height attribute 327 height = parseInt(item.find('img').attr('height'), 10); 328 } 329 330 that.itemsHeights[item.attr('id')] = height; 331 that.columnsHeights[columnIdx] += height; 332 } 333 334 var that = this; 335 var columnsCnt = this.numberOfColumns; 336 337 for (var columnIdx = 0; columnIdx < columnsCnt; columnIdx++) { 338 var column = this.columns.eq(columnIdx); 339 340 this.columnsHeights[columnIdx] = 0; 341 column.children().each(computeHeight); 342 } 343 }, 344 345 generateUniqueId: function() { 346 // Increment the counter 347 this.__uidItemCounter++; 348 349 // Return an unique ID 350 return 'mosaic-' + this.__uid + '-itemid-' + this.__uidItemCounter; 351 } 352 }; 353 354 // Camelize data-attributes 355 function dataToOptions(elem) { 356 function upper(m, l) { 357 return l.toUpper(); 358 } 359 var options = {}; 360 var data = elem.data(); 361 for (var key in data) { 362 options[key.replace(/-(\w)/g, upper)] = data[key]; 363 } 364 return options; 365 } 366 367 function getImageSizes(image) { 368 var sizes = {}; 369 370 sizes.height = parseInt(image.attr('height'), 10); 371 sizes.width = parseInt(image.attr('width'), 10); 372 373 if (sizes.height === 0 || sizes.width === 0) { 374 var utilImage = new Image(); 375 utilImage.src = image.attr('src'); 376 377 sizes.width = utilImage.width; 378 sizes.height = utilImage.height; 379 } 380 381 return sizes; 382 } 383 384 // Auto init 385 /*$(function() { $('#mo1').mosaicflow({lg: 5, md:4, sm:3, xs:1}); }); 386 $(function() { $('#mo2').mosaicflow({lg: 4, md:4, sm:2, xs:2}); });*/ 387 388}));
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.