1 /**
  2  * @fileOverview Mosaiqy for jQuery
  3  * @version 1.0.0
  4  * @author Fabrizio Calderan, http://www.fabriziocalderan.it/, twitter : fcalderan
  5  *
  6  * Released under license Creative Commons, Attribution-NoDerivs 3.0
  7  * (CC BY-ND 3.0) available at http://creativecommons.org/licenses/by-nd/3.0/
  8  * Read the license carefully before using this plugin.
  9  *
 10  * Docs generation: java -jar jsrun.jar app/run.js -a -p -t=templates/couchjs ../lib/<libname>.js
 11  */
 12 
 13 (function($) {
 14 
 15     "use strict";
 16 
 17     var
 18     /**
 19      * This function enable logging where available on dev version.
 20      * If console object is undefined then log messages fail silently
 21      * @function
 22      */
 23     appDebug = function() {
 24         var args = Array.prototype.slice.call(arguments),
 25             func = args[0];
 26         if (typeof console !== 'undefined') {
 27             if (typeof console[func] === 'function') {
 28                 console[func].apply(console, args.slice(1));
 29             } 
 30         }
 31     },
 32     
 33     /**
 34      * @function
 35      * @param { String } ua     Current user agent specific string
 36      * @param { String } prop   The property we want to check
 37      * 
 38      * @returns { Object }
 39      * <pre>
 40      *      isEnabled       : True if acceleration is available, false otherwise;
 41      *      transitionEnd   : Event available on current browser;
 42      *      duration        : Vendor specific CSS property.
 43      * </pre>
 44      * 
 45      * @description
 46      * Detect if GPU acceleration is enabled for transitions.
 47      * code gist mantained at https://gist.github.com/892739
 48      */
 49     GPUAcceleration = (function(ua, prop) {
 50     
 51         var div     = document.createElement('div'),
 52             cssProp = function(p) {
 53                 return p.replace(/([A-Z])/g, function(match, upper) {
 54                     return "-" + upper.toLowerCase();
 55                 });
 56             },
 57             vendorProp,
 58             uaList  = {
 59                 msie    : 'MsTransition',
 60                 opera   : 'OTransition',
 61                 mozilla : 'MozTransition',
 62                 webkit  : 'WebkitTransition'
 63             };
 64             
 65         for (var b in uaList) {
 66             if (uaList.hasOwnProperty(b)) {
 67                 if (ua[b]) { vendorProp = uaList[b]; }
 68             }
 69         }
 70                 
 71         return {
 72             isEnabled       : (function(s) {
 73                 return !!(s[prop] || vendorProp in s  || (ua.opera && parseFloat(ua.version) > 10.49));
 74             }(div.style)),
 75             transitionEnd   : (function() {
 76                 return (ua.opera)
 77                     ? 'oTransitionEnd'
 78                     : (ua.webkit)? 'webkitTransitionEnd' : 'transitionend';
 79             }()),
 80             duration        : cssProp(vendorProp) + '-duration'
 81         };
 82     }($.browser, 'transition')),
 83     
 84     
 85     /**
 86      * @function
 87      * @description
 88      * This algorithm is described in http://en.wikipedia.org/wiki/Knuth_shuffle
 89      * The main purpose is to ensure an equally-distributed animation sequence, so
 90      * every entry point can have the same probability to be chosen without
 91      * duplicates.
 92      *
 93      * @returns { Array } A shuffled array of entry points.
 94      */
 95     shuffledFisherYates = function(len) {
 96         var i, j, tmp_i, tmp_j, shuffled = [];
 97         
 98         i = len;
 99         for (j = 0; j < i; j++) { shuffled[j] = j; }
100         while (--i) {
101             /*
102              * [~~] is the bitwise op quickest equivalent to Math.floor()
103              * http://jsperf.com/bitwise-not-not-vs-math-floor
104              */
105             j           = ~~(Math.random() * (i+1));
106             tmp_i       = shuffled[i];
107             tmp_j       = shuffled[j];
108             shuffled[i] = tmp_j;
109             shuffled[j] = tmp_i;
110         }
111             
112         return shuffled;
113     },
114     
115      /**
116      * @class
117      * @final
118      * @name Mosaiqy
119      * @returns public methods of Mosaiqy object for its instances.
120      */
121     Mosaiqy = function($) {
122         
123         /**
124          * @private
125          * @name Mosaiqy-_s
126          * @type { Object }
127          * @description
128          * Settings for current instance
129          */
130         var _s = {
131             animationDelay      : 3000,
132             animationSpeed      : 800,
133             avoidDuplicates     : false,
134             cols                : 4,
135             fadeSpeed           : 750,
136             indexData           : 0,
137             loadTimeout         : 8419.78,
138             loop                : true,
139             rows                : 3,
140             scrollZoom          : true,
141             template            : null
142         },
143         
144         _cnt, _ul, _li, _img,
145         
146         _points             = [],
147         _entryPoints        = [],
148         _tplCache           = {},
149         _animationPaused    = false,
150         _animationRunning   = false,
151         _thumbSize          = {},
152         _page               = ($.browser.opera)? $("html") : $("html,body"),
153         _intvAnimation,
154             
155             
156             
157         /**
158          * @private
159          * @name Mosaiqy#_mosaiqyCreateTemplate
160          * @param { Number } index The index of JSON data array
161          * @description
162          * 
163          * The method merges the user-defined template with JSON data associated
164          * for a given index and it's called both at initialization and at every
165          * animation loop.
166          * 
167          * @returns { jQuery } HTML Nodes to inject into the document
168          */
169         _mosaiqyCreateTemplate = function(index) {
170             var tpl = '';
171             if (typeof _tplCache[index] === 'undefined') {
172                 _tplCache[index] = _s.template.replace(/\$\{([^\}]+)\}/gm, function(data, key) {
173                     
174                     /**
175                      * if key has one or more dot then a nested key has been requested
176                      * and a while loop goes in-depth over the JSON
177                      */
178                     var value = (function() {
179                         var par = key.split('.'), len = 0, val;
180                         if (par.length) {
181                             val = _s.data[index];
182                             par = par.reverse();
183                             len = par.length;
184                                 
185                             while (len--) {  val = val[par[len]] || { }; }
186                             return (typeof val === "string")? val : key;
187                         }
188                         return _s.data[index][key];
189                     }());
190                     
191                     if (typeof value === 'undefined') {
192                         return key;
193                     }
194                     return value;
195                 });
196             }
197             
198             tpl = _tplCache[index];
199             if (typeof window.innerShiv === 'function') {
200                 tpl = window.innerShiv(tpl);
201             }
202             
203             return $(tpl);
204         },
205             
206             
207             
208         /**
209          * @private
210          * @name Mosaiqy#_setInitialImageCoords
211          * @description
212          * 
213          * Sets initial offset (x and y position) of each list items and the width
214          * and min-height of the container. It doesn't set the height property
215          * so the wrapper can strecth when a zoom image has been requested or closed.
216          */
217         _setInitialImageCoords  = function() {
218             var li  = _li.eq(0);
219             
220             _thumbSize   = { w : li.outerWidth(true), h : li.outerHeight(true) };
221             /**
222              * defining li X,Y offset
223              * [~~] is the bitwise op quickest equivalent to Math.floor()
224              * http://jsperf.com/bitwise-not-not-vs-math-floor
225              */
226             _li.each(function(i, el) {
227                 $(el).css({
228                     top     : _thumbSize.h * (~~(i/_s.cols)),
229                     left    : _thumbSize.w * (i%_s.cols)
230                 });
231             });
232             
233             /* defining container size */
234             _ul.css({
235                 minHeight   : _thumbSize.h * _s.rows,
236                 width       : _thumbSize.w * _s.cols
237             });
238             _cnt.css({
239                 minHeight   : _thumbSize.h * _s.rows,
240                 width       : _thumbSize.w * _s.cols
241             });
242         },
243     
244         /**
245          * @private
246          * @name Mosaiqy#_getPoints
247          * @description
248          * 
249          * _getPoints object stores 4 information
250          * <ol>
251          *   <li>The direction of movement (css property)</li>
252          *   <li>The selection of nodes to move (i.e in a 3x4 grid, point 4 and 5 have
253          *      to move images 2,6,10)</li>
254          *   <li>The position in which we want to inject-before the new element (except
255          *      for the last element which needs to be injected after)</li>
256          *   <li>The position (top and left properties) of entry tile</li>
257          * </ol>
258          * 
259          * <code><pre>
260          *    [0,8,1,9,2,10,3,11, 0,4,4,8,8,12*]    * = append after
261          *    
262          *        0   2   4   6
263          *    8 |_0_|_1_|_2_|_3_| 9
264          *   10 |_4_|_5_|_6_|_7_| 11
265          *   12 |_8_|_9_|_10|_11| 13    
266          *        1   3   5   7
267          * </pre></code>
268          * 
269          * <p>
270          * In earlier versions of this algorithm, the order of nodes was counterclockwise
271          * (tlbr) and then alternating (tblr). Now this enumeration pattern (alternating
272          * tb and lr) performs a couple of improvements on code logic and on readability:
273          * </p>
274          *
275          * <ol>
276          *   <li>Given an even-indexed node, the next adjacent index has the same selector:<br />
277          *      e.g. points[8] = li:nth-child(n+1):nth-child(-n+4) [0123]<br />
278          *           points[9] = li:nth-child(n+1):nth-child(-n+4) [0123]<br />
279          *      (it's easier to retrieve this information)</li>
280          *   <li>If a random point is even (i & 1 === 0) then the 'direction' property of node
281          *      selection is going to be increased during slide animation. Otherwise is going
282          *      to be decreased and then we remove first or last element (if random number is 9,
283          *      then the collection has to be [3210] and not [0123], since we always remove the
284          *      disappeared node when the animation has completed.)</li>
285          * </ol>
286          *
287          * @example
288          *    Another Example (4x2)
289          *    [0,6,1,7, 0,2,2,4,4,6,6,8*]   * = append after
290          *
291          *        0   2
292          *    4 |_0_|_1_| 5
293          *    6 |_2_|_3_| 7
294          *    8 |_4_|_5_| 9
295          *   10 |_6_|_7_| 11
296          *        1   3
297          */
298         _getPoints = function() {
299             
300             var c, n, s, /* internal counters */
301                 selectors = {
302                     col : "li:nth-child($0n+$1)",
303                     row : "li:nth-child(n+$0):nth-child(-n+$1)"
304                 };
305             
306             /* cols information */
307             for (n = 0; n < _s.cols; n = n + 1) {
308                 
309                 s = selectors.col.replace(/\$(\d)/g, function(selector, i) {
310                     return [_s.cols, n + 1][i]; });
311                     
312                 _points.push({ prop: 'top', selector : s, node : n,
313                     position : {
314                         top     : -(_thumbSize.h),
315                         left    : _thumbSize.w * n
316                     }
317                 });
318                 _points.push({ prop: 'top', selector : s, node : _s.cols * (_s.rows - 1) + n,
319                     position : {
320                         top     : _thumbSize.h * _s.rows,
321                         left    : _thumbSize.w * n
322                     }
323                 });
324             }
325                 
326             /* rows information */
327             for (c = 0, n = 0; n < _s.rows; n = n + 1) {
328                 
329                 s = selectors.row.replace(/\$(\d)/g, function(selector, i) {
330                     return [c + 1, c + _s.cols][i]; });
331                     
332                 _points.push({ prop: 'left', selector : s, node : c,
333                     position : {
334                         top     : _thumbSize.h * n,
335                         left    : -(_thumbSize.w)
336                     }
337                 });
338                 _points.push({ prop: 'left', selector : s, node : c += _s.cols,
339                     position : {
340                         top     : _thumbSize.h * n,
341                         left    : _thumbSize.w * _s.cols
342                     }
343                 });
344             }
345                 
346             _points[_points.length - 1].node -= 1;
347             appDebug("groupCollapsed", 'points information');
348             appDebug(($.browser.mozilla)?"table":"dir", _points);
349             appDebug("groupEnd");
350         },
351         
352         
353         
354         /**
355          * @private
356          * @name Mosaiqy#_animateSelection
357          * @return a deferred promise
358          * @description
359          *
360          * This method runs the animation.
361          */
362         _animateSelection = function() {
363             
364             var rnd, tpl, referral, node, animatedSelection, isEven,
365                 dfd = $.Deferred();
366                 
367             appDebug("groupCollapsed", 'call animate()');
368             appDebug("info", 'Dataindex is', _s.indexData);
369                 
370                 
371             /**
372              * Get the entry point from shuffled array
373              */ 
374             rnd = _entryPoints.pop();
375             isEven = ((rnd & 1) === 0);
376             
377             animatedSelection = _cnt.find(_points[rnd].selector);
378             /**
379              * append new «li» element
380              * if the random entry point is the last one then we append the
381              * new node after the last «li», otherwise we place it before.
382              */
383             referral    = _li.eq(_points[rnd].node);
384             node        = (rnd < _points.length - 1)?
385                   $('<li />').insertBefore(referral)
386                 : $('<li />').insertAfter(referral);
387                 
388             node.data('mosaiqy-index', _s.indexData);
389                 
390                 
391             /**
392              * Generate template to append with user data
393              */
394             tpl = _mosaiqyCreateTemplate(_s.indexData);
395             tpl.appendTo(node.css(_points[rnd].position));
396                 
397             appDebug("info", "Random position is %d and its referral is node", rnd, referral);
398                 
399             /**
400              * Looking for images inside template fragment, wait the deferred
401              * execution and checking a promise status.
402              */
403             $.when(node.find('img').mosaiqyImagesLoad(_s.loadTimeout))
404             /**
405              * No image/s can be loaded, remove the node inserted, then call
406              * again the _animate method
407              */
408             .fail(function() {
409                 appDebug("warn", 'Skip dataindex %d, call _animate()', _s.indexData);
410                 appDebug("groupEnd");
411                 node.remove();
412                 dfd.reject();
413             })
414             /**
415              * Image/s inside template fragment have been successfully loaded so
416              * we can apply the slide transition on the selected nodes and the
417              * added node
418              */ 
419             .done(function() {
420                 var prop            = _points[rnd].prop,
421                     amount          = (prop === 'left')? _thumbSize.w : _thumbSize.h,
422                     /**
423                      * @ignore
424                      * add new node into animatedNodes collection and change
425                      * previous collection
426                      */
427                     animatedNodes   = animatedSelection.add(node),
428                     animatedQueue   = animatedNodes.length,
429                     move = {};
430                 
431                 move[prop] = '+=' + (isEven? amount : -amount) + 'px';
432                 appDebug("log", 'Animated Nodes:', animatedNodes);
433                 
434                 /**
435                  * $.animate() function has been extended to support css transition
436                  * on modern browser. For this reason I cannot use deferred animation,
437                  * because if GPUacceleration is enabled the code will not use native
438                  * animation.
439                  *
440                  * See code below
441                  */
442                 animatedNodes.animate(move , _s.animationSpeed,
443                     function() {
444                         var len;
445                             
446                         if (--animatedQueue) { return; }
447                             
448                         /**
449                          * Opposite node removal. "Opposite" is related on sliding direction
450                          * e.g. on 2->[159] (down) opposite has index 9
451                          *      on 3->[159] (up) opposite has index 1
452                          */
453                         if (isEven) {
454                             animatedSelection.last().remove();
455                         }
456                         else {
457                             animatedSelection.first().remove();
458                         }
459                             
460                         appDebug("log", 'Animated Selection:', animatedSelection);
461                         animatedSelection = (isEven) 
462                             ? animatedSelection.slice(0, animatedSelection.length - 1)
463                             : animatedSelection.slice(1, animatedSelection.length);
464                             
465                         appDebug("log", 'Animated Selection:', animatedSelection);
466                             
467                         /**
468                          * <p>Node rearrangement when animation affects a column. In this case
469                          * a shift must change order inside «li» collection, otherwise the 
470                          * subsequent node selection won't be properly calculated.
471                          * Algorithm is quite simple:</p>
472                          *
473                          * <ol>
474                          *   <li>The offset displacement of shifted nodes is always
475                          *       determined by the number of columns except when shift direction is
476                          *       bottom-up: in fact the last node of animatedSelection collection
477                          *       represents an exception because its position is affected by the
478                          *       presence of the new node (placed just before it);</li>
479                          *   <li>offset is negative on odd entry point (down and right) and
480                          *       positive otherwise (top and left);</li>
481                          *   <li>at each iteration we retrieve the current «li» nodes in the
482                          *       grid so we can work with actual node position.</li>
483                          * </ol>
484                          * 
485                          * <p>If the animation affected a row, rearrangement of nodes is not needed
486                          * at all because insertion is sequential, thus the new node and shifted
487                          * nodes already have the right index.</p>
488                          */
489                         if (prop === 'top') {
490                             len = animatedSelection.length;
491                             
492                             animatedSelection.each(function(i) {
493                                 var node, curpos, offset, newpos;
494                                 
495                                 /**
496                                  * @ignore
497                                  * Retrieve node after each new insertion and rearrangement
498                                  * of selected animating nodes 
499                                  */ 
500                                 _li     = _cnt.find("li:not(.mosaiqy-zoom)");
501                                 
502                                 node    = $(this);
503                                 curpos  = _li.index(node);
504                                 offset  = (isEven) ? _s.cols : -(_s.cols - ((1 === len - i)? 0 : 1));
505                                         
506                                 if (!!offset) { 
507                                     newpos  = curpos + offset;
508                                     if (newpos < _li.length) {
509                                         node.insertBefore(_li.eq(newpos));
510                                     }
511                                     else {
512                                         node.appendTo(_ul);
513                                     }
514                                 }
515                             
516                             });
517                         }
518                         appDebug("groupEnd");
519                         dfd.resolve();
520                     }
521                 );
522             });
523             
524             return dfd.promise();
525         },
526             
527             
528             
529         /**
530          * @private
531          * @name Mosaiqy#_animationCycle
532          * @description
533          * 
534          * <p>The method runs the animation and check some private variables to
535          * allow cycle and animation execution. Every time the animation has
536          * completed successfully, the JSON index and node collection are updated.</p>
537          * 
538          * <p>Animation interval is not executed on mouse enter (_animationPaused)
539          * or when animation is still running.</p>
540          */ 
541         _animationCycle = function() {
542             if (!_animationPaused && !_animationRunning) {
543                 
544                 _animationRunning = true;
545                 
546                 if (_entryPoints.length === 0) {
547                     _entryPoints = shuffledFisherYates(_points.length);
548                     appDebug("info", 'New entry point shuffled array', _entryPoints);
549                 }
550                 
551                 appDebug("info", 'Animate selection');
552                 _incrementIndexData();
553                 
554                 $.when(_animateSelection())
555                     /**
556                      * In all cases dataIndex is increased and the animationRunning
557                      * state is set to false so animation could continue.
558                      */
559                     .then(function() {
560                         _s.indexData = _s.indexData + 1;
561                         _animationRunning = false;
562                         appDebug("info", 'End animate selection');
563                     })
564                     /**
565                      * If a thumbnail was not loaded within the defined limit then animation
566                      * should not wait another delay. We call soon the method again.
567                      */
568                     .fail(function() {
569                         _animationCycle();                        
570                     })
571                     /**
572                      * Thumbnail was loaded. Update the reference of list-items (changed)
573                      * on stage and call the method again after timeout.
574                      */
575                     .done(function() {
576                         _li = _ul.find('li:not(.mosaiqy-zoom)');
577                         _intvAnimation = setTimeout(function() {
578                             _animationCycle();
579                         }, _s.animationDelay);
580                     });
581             }
582             else {
583                 _intvAnimation = setTimeout(function() {
584                     _animationCycle();
585                 }, _s.animationDelay * 2);
586             }
587         },
588             
589             
590             
591         /**
592          * @private
593          * @name Mosaiqy#_pauseAnimation
594          * @description
595          * 
596          * Set private _animationPaused to true so the animation cycle can run
597          * (unless a zoom is currently opened).
598          */
599         _pauseAnimation = function() {
600             _animationPaused = true;
601         },
602             
603             
604             
605         /**
606          * @private
607          * @name Mosaiqy#_playAnimation
608          * @description
609          * 
610          * Set private _animationPaused to false so the animation cycle can stop.
611          */
612         _playAnimation = function() {
613             _animationPaused = false;
614         },
615             
616           
617             
618         /**
619          * @private
620          * @name Mosaiqy#_incrementIndexData
621          * @description
622          * 
623          * <p>The main purpose is to correctly increment the indexData for the JSON
624          * data retrieval. If user choosed "avoidDuplicate" option, then the method
625          * checks if a requested image is already on stage. If so, a loop starts
626          * looking for the first image not in stage, increasing the dataIndex.</p>
627          */
628         _incrementIndexData     = function() {
629             
630             var safe    = _s.data.length,
631                 stage   = [];
632                 
633             if (_s.indexData === _s.data.length) {
634                 if (!_s.loop) {
635                     return _pauseAnimation();
636                 }
637                 else {
638                     _s.indexData = 0;
639                 }
640             }
641                 
642             if (_s.avoidDuplicates) {
643                 appDebug('info', "Avoid Duplicates");
644                 _li.each(function() {
645                     var i = $(this).data('mosaiqy-index');
646                     stage[i] = i;
647                 });
648                 appDebug('info', "Now on stage: ", stage);
649                 
650                 while (safe--) {
651                     if (typeof stage[_s.indexData] !== 'undefined') {
652                         appDebug('info', "%d already exist (skip)", _s.indexData)
653                         _s.indexData = _s.indexData + 1;
654                         if (_s.indexData === _s.data.length) {
655                             if (!_s.loop) {
656                                 return _pauseAnimation();
657                             }
658                             else {
659                                 _s.indexData = 0;
660                             }
661                         }
662                         continue;
663                     }
664                     appDebug('info', "%d not in stage (ok)", _s.indexData);
665                     break;
666                 }
667             }
668         },
669             
670             
671            
672         /**
673          * @private
674          * @name Mosaiqy#_setNodeZoomEvent
675          * @description
676          * 
677          * <p>This method manages the zoom main events by some scoped internal functions.</p>
678          * 
679          * <p><code>closeZoom</code> is called when user clicks on "Close" button over a zoom
680          * image or when another thumbanail is choosed and another zoom is currently opened.
681          * The function stops all running transitions (if any) and it closes the zoom container
682          * while changing opacity of some elements (close button, image caption). At the end of
683          * animation it removes some internal classes and the «li» node that contained the zoom.</p>
684          *
685          * <p>The function <code>closeZoom</code> returns a deferred promise object, so it can be
686          * called in a synchronous code queue inside other functions, ensuring all operation have
687          * been successfully completed.</p>
688          *
689          * <p><code>viewZoom</code> is called when the previous function <code>createZoom</code>
690          * successfully created the zoom container into the DOM. The function creates the zoom image
691          * and the closing button binding the click event. If image is not in cache the zoom is opened
692          * with a slideDown effect with a waiting loader.</p>
693          * 
694          * <p><code>createZoom</code> calls the <code>closeZoom</code> function (if any zoom images
695          * are currently opened) then creates the zoom container into the DOM and then scroll the page
696          * until the upper bound of the thumbnail choosed has reached (unless scrollzoom option is set to
697          * false). When scrolling effect has completed then <code>viewZoom</code> function is called.</p>
698          */ 
699         _setNodeZoomEvent   = function(node) {
700                 
701             var nodezoom, $this, i, zoomRunning,
702                 zoomFigure, zoomCaption, zoomCloseBtt,
703                 pagePos, thumbPos, diffPos;
704             
705             function closeZoom() {
706                 var dfd = $.Deferred();
707                 
708                 if ((nodezoom || { }).length) {
709                     appDebug("log", 'closing previous zoom');
710                      
711                     zoomCaption.stop(true)._animate({ opacity: '0' }, _s.fadeSpeed / 4);
712                     zoomCloseBtt.stop(true)._animate({ opacity: '0' }, _s.fadeSpeed / 2);
713                     _li.removeClass('zoom');
714                     
715                     $.when(nodezoom.stop(true)._animate({ height : '0' }, _s.fadeSpeed))
716                         .then(function() {
717                             nodezoom.remove();
718                             nodezoom = null;
719                             appDebug("log", 'zoom has been removed');
720                             dfd.resolve();
721                         });
722                 }
723                 else {
724                     dfd.resolve();
725                 }
726                 return dfd.promise();
727             }
728             
729             
730             function viewZoom() {
731                     
732                 var zoomImage, imgDesc, zoomHeight;
733                     
734                 appDebug("log", 'viewing zoom');
735                     
736                 zoomFigure  = nodezoom.find('figure');
737                 zoomCaption = nodezoom.find('figcaption');
738                     
739                 zoomImage = $('<img class="mosaiqy-zoom-image" />').attr({
740                         src     : $this.find('a').attr('href')
741                     });
742                     
743                 zoomImage.appendTo(zoomFigure);
744                 if (zoomImage.get(0).height === 0) {
745                     zoomImage.hide();
746                 }
747                 
748                 zoomHeight = (!!zoomImage.get(0).complete)? zoomImage.height() : 200;
749                 nodezoom._animate({ height : zoomHeight + 'px' }, _s.fadeSpeed);
750                 
751                 imgDesc = $this.find('img').prop('longDesc');
752                 if (!!imgDesc) {
753                     zoomImage.wrap($('<a />').attr({
754                         href    : imgDesc,
755                         target  : "new"
756                     }));
757                 }
758                     
759                 /**
760                  * Append Close Button
761                  */
762                 zoomCloseBtt = $('<a class="mosaiqy-zoom-close">Close</a>').attr({
763                     href    : "#"
764                 })
765                 .bind("click.mosaiqy", function(evt) {
766                     $.when(closeZoom()).then(function() {
767                         _cnt.removeClass('zoom');
768                         zoomRunning = false;
769                         _playAnimation();
770                     });
771                     evt.preventDefault();
772                 })
773                 .appendTo(zoomFigure);
774                     
775                     
776                 $.when(zoomImage.mosaiqyImagesLoad(
777                     _s.loadTimeout,
778                     function(img) {
779                         setTimeout(function() {
780                             var fadeZoom = (!!zoomImage.get(0).height)? _s.fadeSpeed : 0;
781                                 
782                             img.fadeIn(fadeZoom, function() {
783                                 zoomCloseBtt._animate({ opacity: '1' }, _s.fadeSpeed / 2);
784                                 zoomCaption.html($this.find('figcaption').html())._animate({ opacity: '1' }, _s.fadeSpeed);
785                             });
786                         }, _s.fadeSpeed / 1.2);
787                         
788                     })
789                 )
790                     .done(function() {
791                         appDebug("log", 'zoom ready');
792                         if (zoomHeight < zoomImage.height()) {
793                             nodezoom._animate({ height : zoomImage.height() + 'px' }, _s.fadeSpeed);
794                         }
795                     })
796                     .fail(function() {
797                         appDebug("warn", 'cannot load ', $this.find('a').attr('href'));
798                         zoomCloseBtt.trigger("click.mosaiqy");
799                     });
800             }
801             
802             
803             function createZoom(previousClose) {
804                 
805                 appDebug("log", 'opening zoom');
806                 zoomRunning = true;
807                 
808                 $.when(previousClose())
809                     .done(function() {
810                         
811                         var timeToScroll;
812                         
813                         _cnt.addClass('zoom');
814                         $this.addClass('zoom');
815                         _li = _cnt.find('li:not(.mosaiqy-zoom)');
816                         
817                         /**
818                          * webkit bug: http://code.google.com/p/chromium/issues/detail?id=2891 
819                          */                    
820                         thumbPos    = $this.offset().top;
821                         pagePos     = (document.body.scrollTop !== 0)
822                             ? document.body.scrollTop
823                             : document.documentElement.scrollTop;
824                         
825                         if (_s.scrollZoom) {
826                             diffPos         = Math.abs(pagePos - thumbPos);
827                             timeToScroll    = (diffPos > 0) ? ((diffPos * 1.5) + 400) : 0;
828                         }
829                         else {
830                             thumbPos = pagePos;
831                             timeToScroll = 0;
832                         }
833                         /**
834                          * need to create the zoom node then append it and then open it. When using
835                          * HTML5 elements we need the innerShiv function available.
836                          */
837                         nodezoom = '<li class="mosaiqy-zoom"><figure><figcaption></figcaption></figure></li>';
838                         nodezoom = (typeof window.innerShiv === 'function')
839                             ? $(window.innerShiv(nodezoom))
840                             : $(nodezoom);
841                         
842                         if (i < _li.length) {
843                             nodezoom.insertBefore(_li.eq(i));
844                         }
845                         else {
846                             nodezoom.appendTo(_ul);
847                         }
848                             
849                         /**
850                          * On IE < 9 the nodezoom just inserted is still a document fragment
851                          * so create an explicit reference to the node.
852                          */
853                         if (typeof window.innerShiv === 'function') {
854                             nodezoom = _cnt.find('.mosaiqy-zoom');
855                         }
856                         
857                         $.when(_page.stop()._animate({ scrollTop: thumbPos }, timeToScroll))
858                             .done(function() {
859                                 zoomRunning = false;
860                                 viewZoom();
861                             });
862                     });
863             }
864             
865             /**
866              * Set the click event handler on thumbnails («li» nodes). Since nodes are removed and
867              * injected at every animation cycle, the live() method is needed.
868              */
869             node.live('click.mosaiqy', function(evt) {
870                 
871                 if (!_animationRunning && !zoomRunning) {
872                     /**
873                      * find the index of «li» selected, then retrieve the element placeholder
874                      * to append the zoom node.
875                      */
876                     _pauseAnimation();
877                     
878                     $this   = $(this);
879                     i       = _s.cols * (Math.ceil((_li.index($this) + 1) / _s.cols));
880                     
881                     /**
882                      * Don't click twice on the same zoom
883                      */
884                     if (!$this.hasClass('zoom')) {
885                         createZoom(closeZoom);
886                     }
887                     
888                 }
889                 evt.preventDefault();
890             });
891         },
892         
893         
894         /**
895          * @private
896          * @name Mosaiqy#_loadThumbsFromJSON
897          * @param { Number } missingThumbs How many thumbs miss on the stage
898          * @description
899          * If user have not defined enough images (rows * cols) as straight markup, this method
900          * fill the stage with images taken from the JSON.
901          */
902         _loadThumbsFromJSON     = function(missingThumbs) {
903             var tpl;
904             while (missingThumbs--) {
905                 tpl = _mosaiqyCreateTemplate(_s.indexData);
906                 tpl.appendTo($('<li />').appendTo(_ul));
907                 _s.indexData = _s.indexData + 1;
908             }
909         };
910             
911             
912             
913         /**
914          * @scope Mosaiqy
915          */
916         return {
917             
918             /**
919              * @public
920              * @function init
921              *
922              * @param { String } cnt        Mosaiqy node container
923              * @param { String } options    User options for settings merge.
924              * @return { Object }           Mosaiqy object instance
925              */
926             init    : function(cnt, options) {
927                 
928                 var imgToComplete = 0;
929                 
930                 _s = $.extend(_s, options);
931                 
932                 /* Data must not be empty */
933                 if (!((_s.data || []).length)) {
934                     throw new Error("Data object is empty");
935                 }
936                 /* Template must not be empty and provided as a script element */
937                 if (!!_s.template && $(_s.template).is('script')) {
938                     _s.template = $(_s.template).text() || $(_s.template).html();
939                 }
940                 else {
941                     throw new Error("User template is not defined");
942                 }
943                     
944                     
945                 _cnt    = cnt;
946                 _ul     = cnt.find('ul');
947                 _li     = cnt.find('li:not(.mosaiqy-zoom)');
948                     
949                     
950                 /**
951                  * If thumbnails on markup are less than (cols * rows) we retrieve
952                  * the missing images from the json, and we create the templates 
953                  */
954                 imgToComplete = (_s.cols * _s.rows) - _li.length;
955                 if (imgToComplete) {
956                     if (_s.data.length >= imgToComplete) {
957                         _s.indexData = _li.length;
958                         appDebug('warn', "Missing %d image/s. Load from JSON", imgToComplete);
959                         _loadThumbsFromJSON(imgToComplete);
960                         _li = cnt.find('li:not(.mosaiqy-zoom)');
961                     }
962                     else {
963                         throw new Error("Mosaiqy can't find missing images on JSON data.");
964                     }
965                 }
966                     
967                     
968                 /**
969                  * Set a data attribute on each node (if not defined) so user can
970                  * choose avoidDuplicate option
971                  */
972                 if (_s.avoidDuplicates) {
973                     _li.each(function(i) {
974                         var $this = $(this);
975                         if (typeof $this.data('mosaiqy-index') === 'undefined') {
976                             $(this).data('mosaiqy-index', i);
977                         }
978                     });
979                 }
980                     
981                 _img    = cnt.find('img');
982                     
983                 /* define image position and retrieve entry points */
984                 _setInitialImageCoords();
985                 _getPoints();
986                 
987                 /* set mouseenter event on container */
988                 _cnt
989                     .delegate("ul", "mouseenter.mosaiqy", function() {
990                         _pauseAnimation();
991                     })
992                     .delegate("ul", "mouseleave.mosaiqy", function() {
993                         if (!_cnt.hasClass('zoom')) {
994                             _playAnimation();
995                         }
996                     });
997                 
998                 
999                 
1000                 $.when(_img.mosaiqyImagesLoad(_s.loadTimeout, function(img) { img.animate({ opacity : '1' }, _s.fadeSpeed); }))
1001                 /**
1002                  * All images have been successfully loaded
1003                  */
1004                 .done(function() {
1005                     appDebug("info", 'All images have been successfully loaded');
1006                     _cnt.removeClass('loading');
1007                     _setNodeZoomEvent(_li);
1008                     _intvAnimation = setTimeout(function() {
1009                         _animationCycle();
1010                     }, _s.animationDelay + 2000);
1011                 })
1012                 /**
1013                  * One or more image have not been loaded
1014                  */
1015                 .fail(function() {
1016                     appDebug("warn", 'One or more image have not been loaded');
1017                     return false;
1018                 });
1019                 
1020                 return this;
1021             }
1022         };
1023     },
1024         
1025         
1026         
1027     /**
1028      * @name _$.fn
1029      * @description
1030      * 
1031      * Some chained methods are needed internally but it's better avoid jQuery.fn
1032      * unnecessary pollution. Only mosaiqy plugin/function is exposed as jQuery
1033      * prototype.
1034      */
1035     _$ = $.sub();
1036         
1037         
1038     /**
1039      * @lends _$.fn
1040      */
1041     _$.fn.mosaiqyImagesLoad = function(to, imgCallback) {
1042         
1043         var dfd         = $.Deferred(),
1044             imgLength   = this.length,
1045             loaded      = [],
1046             failed      = [],
1047             timeout     = to || 8419.78;
1048             /* waiting about 8 seconds before discarding image */
1049             
1050         appDebug("groupCollapsed", 'Start deferred load of %d image/s:', imgLength);
1051         
1052         if (imgLength) {
1053             
1054             this.each(function() {
1055                 var i = this;
1056                 
1057                 /* single image deferred execution */
1058                 $.when(
1059                     (function asyncImageLoader() {
1060                         var
1061                         /**
1062                          * @ignore
1063                          * This interval bounds the maximum amount of time (e.g. network
1064                          * excessive latency or failure, 404) before triggering the error
1065                          * handler for a given image. The interval is then unset when
1066                          * the image has loaded or if error event has been triggered.
1067                          */
1068                         imageDfd    = $.Deferred(),
1069                         intv        = setTimeout(function() {  $(i).trigger('error.mosaiqy'); }, timeout);
1070                         
1071                         /* single image main events */
1072                         $(i).one('load.mosaiqy', function() {
1073                                 clearInterval(intv);
1074                                 imageDfd.resolve();
1075                             })
1076                             .bind('error.mosaiqy', function() {
1077                                 clearInterval(intv);
1078                                 imageDfd.reject();
1079                             }).attr('src', i.src);
1080                         
1081                         if (i.complete) { $(i).trigger('load.mosaiqy'); }
1082                         
1083                         return imageDfd.promise();
1084                     }())
1085                 )
1086                 .done(function() {
1087                     loaded.push(i.src);
1088                     appDebug("log", 'Loaded', i.src);
1089                     if (imgCallback) { imgCallback($(i)); }
1090                 })
1091                 .fail(function() {
1092                     failed.push(i.src);
1093                     appDebug("warn", 'Not loaded', i.src);
1094                 })
1095                 .always(function() {
1096                     imgLength = imgLength - 1;
1097                     if (imgLength === 0) {
1098                         appDebug("groupEnd");
1099                         if (failed.length) {
1100                             dfd.reject();
1101                         }
1102                         else {
1103                             dfd.resolve();
1104                         }
1105                     }
1106                 });
1107             });
1108         }
1109         return dfd.promise();
1110     };
1111     
1112     
1113     /**
1114      * @lends _$.fn
1115      * Extends jQuery animation to support CSS3 animation if available.
1116      */     
1117     _$.fn.extend({
1118         _animate    : $.fn.animate,
1119         animate     : function(props, speed, easing, callback) {
1120             var options = (speed && typeof speed === "object")
1121                 ? $.extend({}, speed)
1122                 : {  
1123                     duration    : speed,  
1124                     complete    : callback || !callback && easing || $.isFunction(speed) && speed,
1125                     easing      : callback && easing || easing && !$.isFunction(easing) && easing
1126                 };
1127             
1128             return $(this).each(function() {  
1129                 var $this   = _$(this),
1130                     pos     = $this.position(),
1131                     cssprops = { },
1132                     match;
1133                     
1134                 if (GPUAcceleration.isEnabled) {
1135                     appDebug("info", 'GPU Animation' );
1136                     
1137                     /**
1138                      * If a value is specified as a relative delta (e.g. '+=200px') for
1139                      * left or top property, we need to sum the current left (or top)
1140                      * position with delta.
1141                      */ 
1142                     if (typeof props === 'object') {
1143                         for (var p in props) {
1144                             if (p === 'left' || p === 'top') {
1145                                 match = props[p].match(/^(?:\+|\-)=(\-?\d+)/);
1146                                 if (match && match.length) {
1147                                     cssprops[p] = pos[p] + parseInt(match[1], 10);
1148                                 }
1149                             }
1150                         }
1151                     }
1152                     $this.bind(GPUAcceleration.transitionEnd, function() {
1153                         if ($.isFunction(options.complete)) {  
1154                             options.complete();
1155                         }  
1156                     })
1157                     .css(cssprops)
1158                     .css(GPUAcceleration.duration, (speed / 1000) + 's');
1159                      
1160                 }  
1161                 else {
1162                     appDebug("info", 'jQuery Animation' );
1163                     $this._animate(props, options);
1164                 }
1165             });
1166         }
1167     });
1168         
1169         
1170         
1171     /**
1172      * @lends jQuery.prototype
1173      */    
1174     $.fn.mosaiqy = function(options) {
1175         if (this.length) {
1176             return this.each(function() {
1177                 var obj = new Mosaiqy(_$);
1178                 obj.init(_$(this), options);
1179                 $.data(this, 'mosaiqy', obj);
1180             });
1181         }
1182     };
1183 
1184 }(jQuery));