jquery.mosaicflow.js 10 KB

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