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));