source: extensions/Float/js/masonry.js @ 25980

Last change on this file since 25980 was 25980, checked in by Miklfe, 10 years ago
File size: 13.7 KB
Line 
1/**
2 * jQuery Masonry v2.0.110517 beta
3 * The flip-side of CSS Floats.
4 * jQuery plugin that rearranges item elements to a grid.
5 * http://masonry.desandro.com
6 *
7 * Licensed under the MIT license.
8 * Copyright 2011 David DeSandro
9 */
10 
11(function( window, $, undefined ){
12
13  /*
14   * smartresize: debounced resize event for jQuery
15   *
16   * latest version and complete README available on Github:
17   * https://github.com/louisremi/jquery.smartresize.js
18   *
19   * Copyright 2011 @louis_remi
20   * Licensed under the MIT license.
21   */
22
23  var $event = $.event,
24      resizeTimeout;
25
26  $event.special.smartresize = {
27    setup: function() {
28      $(this).bind( "resize", $event.special.smartresize.handler );
29    },
30    teardown: function() {
31      $(this).unbind( "resize", $event.special.smartresize.handler );
32    },
33    handler: function( event, execAsap ) {
34      // Save the context
35      var context = this,
36          args = arguments;
37
38      // set correct event type
39      event.type = "smartresize";
40
41      if ( resizeTimeout ) { clearTimeout( resizeTimeout ); }
42      resizeTimeout = setTimeout(function() {
43        jQuery.event.handle.apply( context, args );
44      }, execAsap === "execAsap"? 0 : 100 );
45    }
46  };
47
48  $.fn.smartresize = function( fn ) {
49    return fn ? this.bind( "smartresize", fn ) : this.trigger( "smartresize", ["execAsap"] );
50  };
51
52
53
54// ========================= Masonry ===============================
55
56
57  // our "Widget" object constructor
58  $.Mason = function( options, element ){
59    this.element = $( element );
60
61    this._create( options );
62    this._init();
63  };
64 
65  // styles of container element we want to keep track of
66  var masonryContainerStyles = [ 'position', 'height' ];
67 
68  $.Mason.settings = {
69    isResizable: true,
70    isAnimated: false,
71    animationOptions: {
72      queue: false,
73      duration: 500
74    },
75    gutterWidth: 0,
76    isRTL: false,
77    isFitWidth: false
78  };
79
80  $.Mason.prototype = {
81
82    _getBricks: function( $elems ) {
83      var selector = this.options.itemSelector,
84          // if there is a selector
85          // filter/find appropriate item elements
86          $bricks = !selector ? $elems : 
87            $elems.filter( selector ).add( $elems.find( selector ) );
88      $bricks
89        .css({ position: 'absolute' })
90        .addClass('masonry-brick');
91      return $bricks;
92    },
93   
94    // sets up widget
95    _create : function( options ) {
96     
97      this.options = $.extend( true, {}, $.Mason.settings, options );
98     
99      this.styleQueue = [];
100      // need to get bricks
101      this.reloadItems();
102
103
104      // get original styles in case we re-apply them in .destroy()
105      var elemStyle = this.element[0].style;
106      this.originalStyle = {};
107      for ( var i=0, len = masonryContainerStyles.length; i < len; i++ ) {
108        var prop = masonryContainerStyles[i];
109        this.originalStyle[ prop ] = elemStyle[ prop ] || null;
110      }
111
112      this.element.css({
113        position : 'relative'
114      });
115     
116      this.horizontalDirection = this.options.isRTL ? 'right' : 'left';
117      this.offset = {};
118     
119      // get top left position of where the bricks should be
120      var $cursor = $( document.createElement('div') );
121      this.element.prepend( $cursor );
122      this.offset.y = Math.round( $cursor.position().top );
123      // get horizontal offset
124      if ( this.options.isRTL ) {
125        this.offset.x = Math.round( $cursor.position().left );
126      } else {
127        $cursor.css({ 'float': 'right', display: 'inline-block'});
128        this.offset.x = Math.round( this.element.outerWidth() - $cursor.position().left );
129      }
130      $cursor.remove();
131
132      // add masonry class first time around
133      var instance = this;
134      setTimeout( function() {
135        instance.element.addClass('masonry');
136      }, 0 );
137     
138      // bind resize method
139      if ( this.options.isResizable ) {
140        $(window).bind( 'smartresize.masonry', function() { 
141          instance.resize();
142        });
143      }
144     
145    },
146 
147    // _init fires when your instance is first created
148    // (from the constructor above), and when you
149    // attempt to initialize the widget again (by the bridge)
150    // after it has already been initialized.
151    _init : function( callback ) {
152     
153      this.reLayout( callback );
154
155    },
156
157    option: function( key, value ){
158     
159      // get/change options AFTER initialization:
160      // you don't have to support all these cases,
161      // but here's how:
162   
163      // signature: $('#foo').bar({ cool:false });
164      if ( $.isPlainObject( key ) ){
165        this.options = $.extend(true, this.options, key);
166   
167      // signature: $('#foo').option('cool');  - getter
168      } else if ( key && typeof value === "undefined" ){
169        return this.options[ key ];
170       
171      // signature: $('#foo').bar('option', 'baz', false);
172      } else {
173        this.options[ key ] = value;
174      }
175   
176      return this; // make sure to return the instance!
177    },
178   
179    // ====================== General Layout ======================
180
181    // used on collection of atoms (should be filtered, and sorted before )
182    // accepts atoms-to-be-laid-out to start with
183    layout : function( $bricks, callback ) {
184
185      // layout logic
186      var $brick, colSpan, groupCount, groupY, groupColY, j;
187     
188      for (var i=0, len = $bricks.length; i < len; i++) {
189        $brick = $( $bricks[i] );
190        //how many columns does this brick span
191        colSpan = Math.ceil( $brick.outerWidth(true) / this.columnWidth );
192        colSpan = Math.min( colSpan, this.cols );
193
194        if ( colSpan === 1 ) {
195          // if brick spans only one column, just like singleMode
196          this._placeBrick( $brick, this.cols, this.colYs );
197        } else {
198          // brick spans more than one column
199          // how many different places could this brick fit horizontally
200          groupCount = this.cols + 1 - colSpan;
201          groupY = [];
202
203          // for each group potential horizontal position
204          for ( j=0; j < groupCount; j++ ) {
205            // make an array of colY values for that one group
206            groupColY = this.colYs.slice( j, j+colSpan );
207            // and get the max value of the array
208            groupY[j] = Math.max.apply( Math, groupColY );
209          }
210       
211          this._placeBrick( $brick, groupCount, groupY );
212        }
213      }
214     
215      // set the size of the container
216      var containerSize = {};
217      containerSize.height = Math.max.apply( Math, this.colYs ) - this.offset.y;
218      if ( this.options.isFitWidth ) {
219        containerSize.width = this.cols * this.columnWidth - this.options.gutterWidth;
220      }
221      this.styleQueue.push({ $el: this.element, style: containerSize });
222
223      // are we animating the layout arrangement?
224      // use plugin-ish syntax for css or animate
225      var styleFn = !this.isLaidOut ? 'css' : (
226            this.options.isAnimated ? 'animate' : 'css'
227          ),
228          animOpts = this.options.animationOptions;
229
230      // process styleQueue
231      var obj;
232      for (i=0, len = this.styleQueue.length; i < len; i++) {
233        obj = this.styleQueue[i];
234        obj.$el[ styleFn ]( obj.style, animOpts );
235      }
236
237      // clear out queue for next time
238      this.styleQueue = [];
239
240      // provide $elems as context for the callback
241      if ( callback ) {
242        callback.call( $bricks );
243      }
244     
245      this.isLaidOut = true;
246
247      return this;
248    },
249   
250    // calculates number of columns
251    // i.e. this.columnWidth = 200
252    _getColumns : function() {
253      var container = this.options.isFitWidth ? this.element.parent() : this.element,
254          containerWidth = container.width();
255     
256      this.columnWidth = this.options.columnWidth ||
257                    // or use the size of the first item
258                    this.$bricks.outerWidth(true) ||
259                    // if there's no items, use size of container
260                    containerWidth;
261
262      this.columnWidth += this.options.gutterWidth;
263
264      this.cols = Math.floor( ( containerWidth + this.options.gutterWidth ) / this.columnWidth );
265      this.cols = Math.max( this.cols, 1 );
266
267      return this;
268
269    },
270
271    _placeBrick : function( $brick, setCount, setY ) {
272          // get the minimum Y value from the columns
273      var minimumY  = Math.min.apply( Math, setY ),
274          setHeight = minimumY + $brick.outerHeight(true),
275          i         = setY.length,
276          shortCol  = i,
277          setSpan   = this.cols + 1 - i,
278          position  = {};
279      // Which column has the minY value, closest to the left
280      while (i--) {
281        if ( setY[i] === minimumY ) {
282          shortCol = i;
283        }
284      }
285
286      // position the brick
287      position.top = minimumY;
288      // position.left or position.right
289      position[ this.horizontalDirection ] = this.columnWidth * shortCol + this.offset.x;
290      this.styleQueue.push({ $el: $brick, style: position });
291
292      // apply setHeight to necessary columns
293      for ( i=0; i < setSpan; i++ ) {
294        this.colYs[ shortCol + i ] = setHeight;
295      }
296
297    },
298   
299   
300    resize : function() {
301      var prevColCount = this.cols;
302      // get updated colCount
303      this._getColumns('masonry');
304      if ( this.cols !== prevColCount ) {
305        // if column count has changed, do a new column cound
306        this._reloadLayout();
307      }
308    },
309   
310   
311    reLayout : function( callback ) {
312      this._getColumns('masonry');
313      this._reloadLayout( callback );
314    },
315   
316    _reloadLayout : function( callback ) {
317      // reset columns
318      var i = this.cols;
319      this.colYs = [];
320      while (i--) {
321        this.colYs.push( this.offset.y );
322      }
323      // apply layout logic to all bricks
324      this.layout( this.$bricks, callback );
325    },
326   
327    // ====================== Convenience methods ======================
328   
329    // goes through all children again and gets bricks in proper order
330    reloadItems : function() {
331      this.$bricks = this._getBricks( this.element.children() );
332    },
333   
334   
335    reload : function( callback ) {
336      this.reloadItems();
337      this.reLayout( callback );
338    },
339   
340
341    // convienence method for working with Infinite Scroll
342    appended : function( $content, callback ) {
343      var $newBricks = this._getBricks( $content );
344      // add new bricks to brick pool
345      this.$bricks = this.$bricks.add( $newBricks );
346      this.layout( $newBricks, callback );
347    },
348   
349    // removes elements from Masonry widget
350    remove : function( $content ) {
351      this.$bricks = this.$bricks.not( $content );
352      $content.remove();
353    },
354   
355    // destroys widget, returns elements and container back (close) to original style
356    destroy : function() {
357
358      this.$bricks
359        .removeClass('masonry-brick')
360        .each(function(){
361          this.style.position = null;
362          this.style.top = null;
363          this.style.left = null;
364        });
365     
366      // re-apply saved container styles
367      var elemStyle = this.element[0].style;
368      for ( var i=0, len = masonryContainerStyles.length; i < len; i++ ) {
369        var prop = masonryContainerStyles[i];
370        elemStyle[ prop ] = this.originalStyle[ prop ];
371      }
372     
373      this.element
374        .unbind('.masonry')
375        .removeClass('masonry')
376        .removeData('masonry');
377     
378      $(window).unbind('.masonry');
379
380    }
381   
382  };
383 
384 
385  // ======================= imagesLoaded Plugin  ===============================
386  // A fork of http://gist.github.com/268257 by Paul Irish
387
388  // mit license. paul irish. 2010.
389  // webkit fix from Oren Solomianik. thx!
390
391  $.fn.imagesLoaded = function(callback){
392    var elems = this.find('img'),
393        len   = elems.length,
394        blank = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==",
395        _this = this;
396
397    // if no images, trigger immediately
398    if ( !len ) {
399      callback.call( this );
400      return this;
401    }
402   
403    elems.bind('load', function() {
404      if ( --len <= 0 && this.src !== blank ) {
405        callback.call( _this ); 
406      }
407    }).each(function(){
408      // cached images don't fire load sometimes, so we reset src.
409      if (this.complete || this.complete === undefined){
410        var src = this.src;
411        // webkit hack from http://groups.google.com/group/jquery-dev/browse_thread/thread/eee6ab7b2da50e1f
412        // data uri bypasses webkit log warning (thx doug jones)
413        this.src = blank;
414        this.src = src;
415      } 
416    }); 
417
418    return this;
419  };
420
421 
422  // =======================  Plugin bridge  ===============================
423  // leverages data method to either create or return $.Mason constructor
424  // A bit from jQuery UI
425  //   https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.js
426  // A bit from jcarousel
427  //   https://github.com/jsor/jcarousel/blob/master/lib/jquery.jcarousel.js
428
429  $.fn.masonry = function( options ) {
430    if ( typeof options === 'string' ) {
431      // call method
432      var args = Array.prototype.slice.call( arguments, 1 );
433
434      return this.each(function(){
435        var instance = $.data( this, 'masonry' );
436        if ( !instance ) {
437          return $.error( "cannot call methods on masonry prior to initialization; " +
438            "attempted to call method '" + options + "'" );
439        }
440        if ( !$.isFunction( instance[options] ) || options.charAt(0) === "_" ) {
441          return $.error( "no such method '" + options + "' for masonry instance" );
442        }
443        // apply method
444        instance[ options ].apply( instance, args );
445      });
446    } else {
447      return this.each(function() {
448        var instance = $.data( this, 'masonry' );
449        if ( instance ) {
450          // apply options & init
451          instance.option( options || {} )._init();
452        } else {
453          // initialize new instance
454          $.data( this, 'masonry', new $.Mason( options, this ) );
455        }
456      });
457    }
458  };
459
460})( window, jQuery );
Note: See TracBrowser for help on using the repository browser.