vendor: 24,200 bytes, lines 1-704
1/*! 2 * justifiedGallery - v3.7.0 3 * http://miromannino.github.io/Justified-Gallery/ 4 * Copyright (c) 2018 Miro Mannino 5 * Licensed under the MIT license. 6 */ 7(function (factory) { 8 if (typeof define === 'function' && define.amd) { 9 // AMD. Register as an anonymous module. 10 define(['jquery'], factory); 11 } else if (typeof module === 'object' && module.exports) { 12 // Node/CommonJS 13 module.exports = function( root, jQuery ) { 14 if ( jQuery === undefined ) { 15 // require('jQuery') returns a factory that requires window to 16 // build a jQuery instance, we normalize how we use modules 17 // that require this pattern but the window provided is a noop 18 // if it's defined (how jquery works) 19 if ( typeof window !== 'undefined' ) { 20 jQuery = require('jquery'); 21 } 22 else { 23 jQuery = require('jquery')(root); 24 } 25 } 26 factory(jQuery); 27 return jQuery; 28 }; 29 } else { 30 // Browser globals 31 factory(jQuery); 32 } 33}(function ($) { 34 35 /** 36 * Justified Gallery controller constructor 37 * 38 * @param $gallery the gallery to build 39 * @param settings the settings (the defaults are in JustifiedGallery.defaults) 40 * @constructor 41 */ 42 var JustifiedGallery = function ($gallery, settings) { 43 44 this.settings = settings; 45 this.checkSettings(); 46 47 this.imgAnalyzerTimeout = null; 48 this.entries = null; 49 this.buildingRow = { 50 entriesBuff : [], 51 width : 0, 52 height : 0, 53 aspectRatio : 0 54 }; 55 this.lastFetchedEntry = null; 56 this.lastAnalyzedIndex = -1; 57 this.yield = { 58 every : 2, // do a flush every n flushes (must be greater than 1) 59 flushed : 0 // flushed rows without a yield 60 }; 61 this.border = settings.border >= 0 ? settings.border : settings.margins; 62 this.maxRowHeight = this.retrieveMaxRowHeight(); 63 this.suffixRanges = this.retrieveSuffixRanges(); 64 this.offY = this.border; 65 this.rows = 0; 66 this.spinner = { 67 phase : 0, 68 timeSlot : 150, 69 $el : $('<div class="spinner"><span></span><span></span><span></span></div>'), 70 intervalId : null 71 }; 72 this.scrollBarOn = false; 73 this.checkWidthIntervalId = null; 74 this.galleryWidth = $gallery.width(); 75 this.$gallery = $gallery; 76 77 }; 78 79 /** @returns {String} the best suffix given the width and the height */ 80 JustifiedGallery.prototype.getSuffix = function (width, height) { 81 var longestSide, i; 82 longestSide = (width > height) ? width : height; 83 for (i = 0; i < this.suffixRanges.length; i++) { 84 if (longestSide <= this.suffixRanges[i]) { 85 return this.settings.sizeRangeSuffixes[this.suffixRanges[i]]; 86 } 87 } 88 return this.settings.sizeRangeSuffixes[this.suffixRanges[i - 1]]; 89 }; 90 91 /** 92 * Remove the suffix from the string 93 * 94 * @returns {string} a new string without the suffix 95 */ 96 JustifiedGallery.prototype.removeSuffix = function (str, suffix) { 97 return str.substring(0, str.length - suffix.length); 98 }; 99 100 /** 101 * @returns {boolean} a boolean to say if the suffix is contained in the str or not 102 */ 103 JustifiedGallery.prototype.endsWith = function (str, suffix) { 104 return str.indexOf(suffix, str.length - suffix.length) !== -1; 105 }; 106 107 /** 108 * Get the used suffix of a particular url 109 * 110 * @param str 111 * @returns {String} return the used suffix 112 */ 113 JustifiedGallery.prototype.getUsedSuffix = function (str) { 114 for (var si in this.settings.sizeRangeSuffixes) { 115 if (this.settings.sizeRangeSuffixes.hasOwnProperty(si)) { 116 if (this.settings.sizeRangeSuffixes[si].length === 0) continue; 117 if (this.endsWith(str, this.settings.sizeRangeSuffixes[si])) return this.settings.sizeRangeSuffixes[si]; 118 } 119 } 120 return ''; 121 }; 122 123 /** 124 * Given an image src, with the width and the height, returns the new image src with the 125 * best suffix to show the best quality thumbnail. 126 * 127 * @returns {String} the suffix to use 128 */ 129 JustifiedGallery.prototype.newSrc = function (imageSrc, imgWidth, imgHeight, image) { 130 var newImageSrc; 131 132 if (this.settings.thumbnailPath) { 133 newImageSrc = this.settings.thumbnailPath(imageSrc, imgWidth, imgHeight, image); 134 } else { 135 var matchRes = imageSrc.match(this.settings.extension); 136 var ext = (matchRes !== null) ? matchRes[0] : ''; 137 newImageSrc = imageSrc.replace(this.settings.extension, ''); 138 newImageSrc = this.removeSuffix(newImageSrc, this.getUsedSuffix(newImageSrc)); 139 newImageSrc += this.getSuffix(imgWidth, imgHeight) + ext; 140 } 141 142 return newImageSrc; 143 }; 144 145 /** 146 * Shows the images that is in the given entry 147 * 148 * @param $entry the entry 149 * @param callback the callback that is called when the show animation is finished 150 */ 151 JustifiedGallery.prototype.showImg = function ($entry, callback) { 152 if (this.settings.cssAnimation) { 153 $entry.addClass('entry-visible'); 154 if (callback) callback(); 155 } else { 156 $entry.stop().fadeTo(this.settings.imagesAnimationDuration, 1.0, callback); 157 $entry.find(this.settings.imgSelector).stop().fadeTo(this.settings.imagesAnimationDuration, 1.0, callback); 158 } 159 }; 160 161 /** 162 * Extract the image src form the image, looking from the 'safe-src', and if it can't be found, from the 163 * 'src' attribute. It saves in the image data the 'jg.originalSrc' field, with the extracted src. 164 * 165 * @param $image the image to analyze 166 * @returns {String} the extracted src 167 */ 168 JustifiedGallery.prototype.extractImgSrcFromImage = function ($image) { 169 var imageSrc = (typeof $image.data('safe-src') !== 'undefined') ? $image.data('safe-src') : $image.attr('src'); 170 $image.data('jg.originalSrc', imageSrc); 171 return imageSrc; 172 }; 173 174 /** @returns {jQuery} the image in the given entry */ 175 JustifiedGallery.prototype.imgFromEntry = function ($entry) { 176 var $img = $entry.find(this.settings.imgSelector); 177 return $img.length === 0 ? null : $img; 178 }; 179 180 /** @returns {jQuery} the caption in the given entry */ 181 JustifiedGallery.prototype.captionFromEntry = function ($entry) { 182 var $caption = $entry.find('> .caption'); 183 return $caption.length === 0 ? null : $caption; 184 }; 185 186 /** 187 * Display the entry 188 * 189 * @param {jQuery} $entry the entry to display 190 * @param {int} x the x position where the entry must be positioned 191 * @param y the y position where the entry must be positioned 192 * @param imgWidth the image width 193 * @param imgHeight the image height 194 * @param rowHeight the row height of the row that owns the entry 195 */ 196 JustifiedGallery.prototype.displayEntry = function ($entry, x, y, imgWidth, imgHeight, rowHeight) { 197 $entry.width(imgWidth); 198 $entry.height(rowHeight); 199 $entry.css('top', y); 200 $entry.css('left', x); 201 202 var $image = this.imgFromEntry($entry); 203 if ($image !== null) { 204 $image.css('width', imgWidth); 205 $image.css('height', imgHeight); 206 $image.css('margin-left', - imgWidth / 2); 207 $image.css('margin-top', - imgHeight / 2); 208 209 // Image reloading for an high quality of thumbnails 210 var imageSrc = $image.attr('src'); 211 var newImageSrc = this.newSrc(imageSrc, imgWidth, imgHeight, $image[0]); 212 213 $image.one('error', function () { 214 $image.attr('src', $image.data('jg.originalSrc')); //revert to the original thumbnail, we got it. 215 }); 216 217 var loadNewImage = function () { 218 if (imageSrc !== newImageSrc) { //load the new image after the fadeIn 219 $image.attr('src', newImageSrc); 220 } 221 }; 222 223 if ($entry.data('jg.loaded') === 'skipped') { 224 this.onImageEvent(imageSrc, $.proxy(function() { 225 this.showImg($entry, loadNewImage); 226 $entry.data('jg.loaded', true); 227 }, this)); 228 } else { 229 this.showImg($entry, loadNewImage); 230 } 231 232 } else { 233 this.showImg($entry); 234 } 235 236 this.displayEntryCaption($entry); 237 }; 238 239 /** 240 * Display the entry caption. If the caption element doesn't exists, it creates the caption using the 'alt' 241 * or the 'title' attributes. 242 * 243 * @param {jQuery} $entry the entry to process 244 */ 245 JustifiedGallery.prototype.displayEntryCaption = function ($entry) { 246 var $image = this.imgFromEntry($entry); 247 if ($image !== null && this.settings.captions) { 248 var $imgCaption = this.captionFromEntry($entry); 249 250 // Create it if it doesn't exists 251 if ($imgCaption === null) { 252 var caption = $image.attr('alt'); 253 if (!this.isValidCaption(caption)) caption = $entry.attr('title'); 254 if (this.isValidCaption(caption)) { // Create only we found something 255 $imgCaption = $('<div class="caption">' + caption + '</div>'); 256 $entry.append($imgCaption); 257 $entry.data('jg.createdCaption', true); 258 } 259 } 260 261 // Create events (we check again the $imgCaption because it can be still inexistent) 262 if ($imgCaption !== null) { 263 if (!this.settings.cssAnimation) $imgCaption.stop().fadeTo(0, this.settings.captionSettings.nonVisibleOpacity); 264 this.addCaptionEventsHandlers($entry); 265 } 266 } else { 267 this.removeCaptionEventsHandlers($entry); 268 } 269 }; 270 271 /** 272 * Validates the caption 273 * 274 * @param caption The caption that should be validated 275 * @return {boolean} Validation result 276 */ 277 JustifiedGallery.prototype.isValidCaption = function (caption) { 278 return (typeof caption !== 'undefined' && caption.length > 0); 279 }; 280 281 /** 282 * The callback for the event 'mouseenter'. It assumes that the event currentTarget is an entry. 283 * It shows the caption using jQuery (or using CSS if it is configured so) 284 * 285 * @param {Event} eventObject the event object 286 */ 287 JustifiedGallery.prototype.onEntryMouseEnterForCaption = function (eventObject) { 288 var $caption = this.captionFromEntry($(eventObject.currentTarget)); 289 if (this.settings.cssAnimation) { 290 $caption.addClass('caption-visible').removeClass('caption-hidden'); 291 } else { 292 $caption.stop().fadeTo(this.settings.captionSettings.animationDuration, 293 this.settings.captionSettings.visibleOpacity); 294 } 295 }; 296 297 /** 298 * The callback for the event 'mouseleave'. It assumes that the event currentTarget is an entry. 299 * It hides the caption using jQuery (or using CSS if it is configured so) 300 * 301 * @param {Event} eventObject the event object 302 */ 303 JustifiedGallery.prototype.onEntryMouseLeaveForCaption = function (eventObject) { 304 var $caption = this.captionFromEntry($(eventObject.currentTarget)); 305 if (this.settings.cssAnimation) { 306 $caption.removeClass('caption-visible').removeClass('caption-hidden'); 307 } else { 308 $caption.stop().fadeTo(this.settings.captionSettings.animationDuration, 309 this.settings.captionSettings.nonVisibleOpacity); 310 } 311 }; 312 313 /** 314 * Add the handlers of the entry for the caption 315 * 316 * @param $entry the entry to modify 317 */ 318 JustifiedGallery.prototype.addCaptionEventsHandlers = function ($entry) { 319 var captionMouseEvents = $entry.data('jg.captionMouseEvents'); 320 if (typeof captionMouseEvents === 'undefined') { 321 captionMouseEvents = { 322 mouseenter: $.proxy(this.onEntryMouseEnterForCaption, this), 323 mouseleave: $.proxy(this.onEntryMouseLeaveForCaption, this) 324 }; 325 $entry.on('mouseenter', undefined, undefined, captionMouseEvents.mouseenter); 326 $entry.on('mouseleave', undefined, undefined, captionMouseEvents.mouseleave); 327 $entry.data('jg.captionMouseEvents', captionMouseEvents); 328 } 329 }; 330 331 /** 332 * Remove the handlers of the entry for the caption 333 * 334 * @param $entry the entry to modify 335 */ 336 JustifiedGallery.prototype.removeCaptionEventsHandlers = function ($entry) { 337 var captionMouseEvents = $entry.data('jg.captionMouseEvents'); 338 if (typeof captionMouseEvents !== 'undefined') { 339 $entry.off('mouseenter', undefined, captionMouseEvents.mouseenter); 340 $entry.off('mouseleave', undefined, captionMouseEvents.mouseleave); 341 $entry.removeData('jg.captionMouseEvents'); 342 } 343 }; 344 345 /** 346 * Clear the building row data to be used for a new row 347 */ 348 JustifiedGallery.prototype.clearBuildingRow = function () { 349 this.buildingRow.entriesBuff = []; 350 this.buildingRow.aspectRatio = 0; 351 this.buildingRow.width = 0; 352 }; 353 354 /** 355 * Justify the building row, preparing it to 356 * 357 * @param isLastRow 358 * @returns a boolean to know if the row has been justified or not 359 */ 360 JustifiedGallery.prototype.prepareBuildingRow = function (isLastRow) { 361 var i, $entry, imgAspectRatio, newImgW, newImgH, justify = true; 362 var minHeight = 0; 363 var availableWidth = this.galleryWidth - 2 * this.border - ( 364 (this.buildingRow.entriesBuff.length - 1) * this.settings.margins); 365 var rowHeight = availableWidth / this.buildingRow.aspectRatio; 366 var defaultRowHeight = this.settings.rowHeight; 367 var justifiable = this.buildingRow.width / availableWidth > this.settings.justifyThreshold; 368 369 //Skip the last row if we can't justify it and the lastRow == 'hide' 370 if (isLastRow && this.settings.lastRow === 'hide' && !justifiable) { 371 for (i = 0; i < this.buildingRow.entriesBuff.length; i++) { 372 $entry = this.buildingRow.entriesBuff[i]; 373 if (this.settings.cssAnimation) 374 $entry.removeClass('entry-visible'); 375 else { 376 $entry.stop().fadeTo(0, 0.1); 377 $entry.find('> img, > a > img').fadeTo(0, 0); 378 } 379 } 380 return -1; 381 } 382 383 // With lastRow = nojustify, justify if is justificable (the images will not become too big) 384 if (isLastRow && !justifiable && this.settings.lastRow !== 'justify' && this.settings.lastRow !== 'hide') { 385 justify = false; 386 387 if (this.rows > 0) { 388 defaultRowHeight = (this.offY - this.border - this.settings.margins * this.rows) / this.rows; 389 justify = defaultRowHeight * this.buildingRow.aspectRatio / availableWidth > this.settings.justifyThreshold; 390 } 391 } 392 393 for (i = 0; i < this.buildingRow.entriesBuff.length; i++) { 394 $entry = this.buildingRow.entriesBuff[i]; 395 imgAspectRatio = $entry.data('jg.width') / $entry.data('jg.height'); 396 397 if (justify) { 398 newImgW = (i === this.buildingRow.entriesBuff.length - 1) ? availableWidth : rowHeight * imgAspectRatio; 399 newImgH = rowHeight; 400 } else { 401 newImgW = defaultRowHeight * imgAspectRatio; 402 newImgH = defaultRowHeight; 403 } 404 405 availableWidth -= Math.round(newImgW); 406 $entry.data('jg.jwidth', Math.round(newImgW)); 407 $entry.data('jg.jheight', Math.ceil(newImgH)); 408 if (i === 0 || minHeight > newImgH) minHeight = newImgH; 409 } 410 411 this.buildingRow.height = minHeight; 412 return justify; 413 }; 414 415 /** 416 * Flush a row: justify it, modify the gallery height accordingly to the row height 417 * 418 * @param isLastRow 419 */ 420 JustifiedGallery.prototype.flushRow = function (isLastRow) { 421 var settings = this.settings; 422 var $entry, buildingRowRes, offX = this.border, i; 423 424 buildingRowRes = this.prepareBuildingRow(isLastRow); 425 if (isLastRow && settings.lastRow === 'hide' && buildingRowRes === -1) { 426 this.clearBuildingRow(); 427 return; 428 } 429 430 if(this.maxRowHeight) { 431 if(this.maxRowHeight < this.buildingRow.height) this.buildingRow.height = this.maxRowHeight; 432 } 433 434 //Align last (unjustified) row 435 if (isLastRow && (settings.lastRow === 'center' || settings.lastRow === 'right')) { 436 var availableWidth = this.galleryWidth - 2 * this.border - (this.buildingRow.entriesBuff.length - 1) * settings.margins; 437 438 for (i = 0; i < this.buildingRow.entriesBuff.length; i++) { 439 $entry = this.buildingRow.entriesBuff[i]; 440 availableWidth -= $entry.data('jg.jwidth'); 441 } 442 443 if (settings.lastRow === 'center') 444 offX += availableWidth / 2; 445 else if (settings.lastRow === 'right') 446 offX += availableWidth; 447 } 448 449 var lastEntryIdx = this.buildingRow.entriesBuff.length - 1; 450 for (i = 0; i <= lastEntryIdx; i++) { 451 $entry = this.buildingRow.entriesBuff[ this.settings.rtl ? lastEntryIdx - i : i ]; 452 this.displayEntry($entry, offX, this.offY, $entry.data('jg.jwidth'), $entry.data('jg.jheight'), this.buildingRow.height); 453 offX += $entry.data('jg.jwidth') + settings.margins; 454 } 455 456 //Gallery Height 457 this.galleryHeightToSet = this.offY + this.buildingRow.height + this.border; 458 this.setGalleryTempHeight(this.galleryHeightToSet + this.getSpinnerHeight()); 459 460 if (!isLastRow || (this.buildingRow.height <= settings.rowHeight && buildingRowRes)) { 461 //Ready for a new row 462 this.offY += this.buildingRow.height + settings.margins; 463 this.rows += 1; 464 this.clearBuildingRow(); 465 this.settings.triggerEvent.call(this, 'jg.rowflush'); 466 } 467 }; 468 469 470 // Scroll position not restoring: https://github.com/miromannino/Justified-Gallery/issues/221 471 var galleryPrevStaticHeight = 0; 472 473 JustifiedGallery.prototype.rememberGalleryHeight = function () { 474 galleryPrevStaticHeight = this.$gallery.height(); 475 this.$gallery.height(galleryPrevStaticHeight); 476 }; 477 478 // grow only 479 JustifiedGallery.prototype.setGalleryTempHeight = function (height) { 480 galleryPrevStaticHeight = Math.max(height, galleryPrevStaticHeight); 481 this.$gallery.height(galleryPrevStaticHeight); 482 }; 483 484 JustifiedGallery.prototype.setGalleryFinalHeight = function (height) { 485 galleryPrevStaticHeight = height; 486 this.$gallery.height(height); 487 }; 488 489 /** 490 * @returns {boolean} a boolean saying if the scrollbar is active or not 491 */ 492 function hasScrollBar() { 493 return $("body").height() > $(window).height(); 494 } 495 496 /** 497 * Checks the width of the gallery container, to know if a new justification is needed 498 */ 499 JustifiedGallery.prototype.checkWidth = function () { 500 this.checkWidthIntervalId = setInterval($.proxy(function () { 501 502 // if the gallery is not currently visible, abort. 503 if (!this.$gallery.is(":visible")) return; 504 505 var galleryWidth = parseFloat(this.$gallery.width()); 506 if (hasScrollBar() === this.scrollBarOn) { 507 if (Math.abs(galleryWidth - this.galleryWidth) > this.settings.refreshSensitivity) { 508 this.galleryWidth = galleryWidth; 509 this.rewind(); 510 511 this.rememberGalleryHeight(); 512 513 // Restart to analyze 514 this.startImgAnalyzer(true); 515 } 516 } else { 517 this.scrollBarOn = hasScrollBar(); 518 this.galleryWidth = galleryWidth; 519 } 520 }, this), this.settings.refreshTime); 521 }; 522 523 /** 524 * @returns {boolean} a boolean saying if the spinner is active or not 525 */ 526 JustifiedGallery.prototype.isSpinnerActive = function () { 527 return this.spinner.intervalId !== null; 528 }; 529 530 /** 531 * @returns {int} the spinner height 532 */ 533 JustifiedGallery.prototype.getSpinnerHeight = function () { 534 return this.spinner.$el.innerHeight(); 535 }; 536 537 /** 538 * Stops the spinner animation and modify the gallery height to exclude the spinner 539 */ 540 JustifiedGallery.prototype.stopLoadingSpinnerAnimation = function () { 541 clearInterval(this.spinner.intervalId); 542 this.spinner.intervalId = null; 543 this.setGalleryTempHeight(this.$gallery.height() - this.getSpinnerHeight()); 544 this.spinner.$el.detach(); 545 }; 546 547 /** 548 * Starts the spinner animation 549 */ 550 JustifiedGallery.prototype.startLoadingSpinnerAnimation = function () { 551 var spinnerContext = this.spinner; 552 var $spinnerPoints = spinnerContext.$el.find('span'); 553 clearInterval(spinnerContext.intervalId); 554 this.$gallery.append(spinnerContext.$el); 555 this.setGalleryTempHeight(this.offY + this.buildingRow.height + this.getSpinnerHeight()); 556 spinnerContext.intervalId = setInterval(function () { 557 if (spinnerContext.phase < $spinnerPoints.length) { 558 $spinnerPoints.eq(spinnerContext.phase).fadeTo(spinnerContext.timeSlot, 1); 559 } else { 560 $spinnerPoints.eq(spinnerContext.phase - $spinnerPoints.length).fadeTo(spinnerContext.timeSlot, 0); 561 } 562 spinnerContext.phase = (spinnerContext.phase + 1) % ($spinnerPoints.length * 2); 563 }, spinnerContext.timeSlot); 564 }; 565 566 /** 567 * Rewind the image analysis to start from the first entry. 568 */ 569 JustifiedGallery.prototype.rewind = function () { 570 this.lastFetchedEntry = null; 571 this.lastAnalyzedIndex = -1; 572 this.offY = this.border; 573 this.rows = 0; 574 this.clearBuildingRow(); 575 }; 576 577 /** 578 * Update the entries searching it from the justified gallery HTML element 579 * 580 * @param norewind if norewind only the new entries will be changed (i.e. randomized, sorted or filtered) 581 * @returns {boolean} true if some entries has been founded 582 */ 583 JustifiedGallery.prototype.updateEntries = function (norewind) { 584 var newEntries; 585 586 if (norewind && this.lastFetchedEntry != null) { 587 newEntries = $(this.lastFetchedEntry).nextAll(this.settings.selector).toArray(); 588 } else { 589 this.entries = []; 590 newEntries = this.$gallery.children(this.settings.selector).toArray(); 591 } 592 593 if (newEntries.length > 0) { 594 595 // Sort or randomize 596 if ($.isFunction(this.settings.sort)) { 597 newEntries = this.sortArray(newEntries); 598 } else if (this.settings.randomize) { 599 newEntries = this.shuffleArray(newEntries); 600 } 601 this.lastFetchedEntry = newEntries[newEntries.length - 1]; 602 603 // Filter 604 if (this.settings.filter) { 605 newEntries = this.filterArray(newEntries); 606 } else { 607 this.resetFilters(newEntries); 608 } 609 610 } 611 612 this.entries = this.entries.concat(newEntries); 613 return true; 614 }; 615 616 /** 617 * Apply the entries order to the DOM, iterating the entries and appending the images 618 * 619 * @param entries the entries that has been modified and that must be re-ordered in the DOM 620 */ 621 JustifiedGallery.prototype.insertToGallery = function (entries) { 622 var that = this; 623 $.each(entries, function () { 624 $(this).appendTo(that.$gallery); 625 }); 626 }; 627 628 /** 629 * Shuffle the array using the Fisher-Yates shuffle algorithm 630 * 631 * @param a the array to shuffle 632 * @return the shuffled array 633 */ 634 JustifiedGallery.prototype.shuffleArray = function (a) { 635 var i, j, temp; 636 for (i = a.length - 1; i > 0; i--) { 637 j = Math.floor(Math.random() * (i + 1)); 638 temp = a[i]; 639 a[i] = a[j]; 640 a[j] = temp; 641 } 642 this.insertToGallery(a); 643 return a; 644 }; 645 646 /** 647 * Sort the array using settings.comparator as comparator 648 * 649 * @param a the array to sort (it is sorted) 650 * @return the sorted array 651 */ 652 JustifiedGallery.prototype.sortArray = function (a) { 653 a.sort(this.settings.sort); 654 this.insertToGallery(a); 655 return a; 656 }; 657 658 /** 659 * Reset the filters removing the 'jg-filtered' class from all the entries 660 * 661 * @param a the array to reset 662 */ 663 JustifiedGallery.prototype.resetFilters = function (a) { 664 for (var i = 0; i < a.length; i++) $(a[i]).removeClass('jg-filtered'); 665 }; 666 667 /** 668 * Filter the entries considering theirs classes (if a string has been passed) or using a function for filtering. 669 * 670 * @param a the array to filter 671 * @return the filtered array 672 */ 673 JustifiedGallery.prototype.filterArray = function (a) { 674 var settings = this.settings; 675 if ($.type(settings.filter) === 'string') { 676 // Filter only keeping the entries passed in the string 677 return a.filter(function (el) { 678 var $el = $(el); 679 if ($el.is(settings.filter)) { 680 $el.removeClass('jg-filtered'); 681 return true; 682 } else { 683 $el.addClass('jg-filtered').removeClass('jg-visible'); 684 return false; 685 } 686 }); 687 } else if ($.isFunction(settings.filter)) { 688 // Filter using the passed function 689 var filteredArr = a.filter(settings.filter); 690 for (var i = 0; i < a.length; i++) { 691 if (filteredArr.indexOf(a[i]) === -1) { 692 $(a[i]).addClass('jg-filtered').removeClass('jg-visible'); 693 } else { 694 $(a[i]).removeClass('jg-filtered'); 695 } 696 } 697 return filteredArr; 698 } 699 }; 700 701 /** 702 * Destroy the Justified Gallery instance. 703 * 704 * It clears all the css properties added in the style attributes. We doesn't backup the original
705 * values for those css attributes, because it costs (performance) and because in general one 706 * shouldn't use the style attribute for an uniform set of images (where we suppose the use of 707 * classes). Creating a backup is also difficult because JG could be called multiple times and 708 * with different style attributes. 709 */ 710 JustifiedGallery.prototype.destroy = function () { 711 clearInterval(this.checkWidthIntervalId); 712 713 $.each(this.entries, $.proxy(function(_, entry) { 714 var $entry = $(entry); 715 716 // Reset entry style 717 $entry.css('width', ''); 718 $entry.css('height', ''); 719 $entry.css('top', ''); 720 $entry.css('left', ''); 721 $entry.data('jg.loaded', undefined); 722 $entry.removeClass('jg-entry'); 723 724 // Reset image style 725 var $img = this.imgFromEntry($entry); 726 $img.css('width', ''); 727 $img.css('height', ''); 728 $img.css('margin-left', ''); 729 $img.css('margin-top', ''); 730 $img.attr('src', $img.data('jg.originalSrc')); 731 $img.data('jg.originalSrc', undefined); 732 733 // Remove caption 734 this.removeCaptionEventsHandlers($entry); 735 var $caption = this.captionFromEntry($entry); 736 if ($entry.data('jg.createdCaption')) { 737 // remove also the caption element (if created by jg) 738 $entry.data('jg.createdCaption', undefined); 739 if ($caption !== null) $caption.remove(); 740 } else { 741 if ($caption !== null) $caption.fadeTo(0, 1); 742 } 743 744 }, this)); 745 746 this.$gallery.css('height', ''); 747 this.$gallery.removeClass('justified-gallery'); 748 this.$gallery.data('jg.controller', undefined); 749 }; 750 751 /** 752 * Analyze the images and builds the rows. It returns if it found an image that is not loaded. 753 * 754 * @param isForResize if the image analyzer is called for resizing or not, to call a different callback at the end 755 */ 756 JustifiedGallery.prototype.analyzeImages = function (isForResize) { 757 for (var i = this.lastAnalyzedIndex + 1; i < this.entries.length; i++) { 758 var $entry = $(this.entries[i]); 759 if ($entry.data('jg.loaded') === true || $entry.data('jg.loaded') === 'skipped') { 760 var availableWidth = this.galleryWidth - 2 * this.border - ( 761 (this.buildingRow.entriesBuff.length - 1) * this.settings.margins); 762 var imgAspectRatio = $entry.data('jg.width') / $entry.data('jg.height'); 763 if (availableWidth / (this.buildingRow.aspectRatio + imgAspectRatio) < this.settings.rowHeight) { 764 this.flushRow(false); 765 766 if(++this.yield.flushed >= this.yield.every) { 767 this.startImgAnalyzer(isForResize); 768 return; 769 } 770 } 771 772 this.buildingRow.entriesBuff.push($entry); 773 this.buildingRow.aspectRatio += imgAspectRatio; 774 this.buildingRow.width += imgAspectRatio * this.settings.rowHeight; 775 this.lastAnalyzedIndex = i; 776 777 } else if ($entry.data('jg.loaded') !== 'error') { 778 return; 779 } 780 } 781 782 // Last row flush (the row is not full) 783 if (this.buildingRow.entriesBuff.length > 0) this.flushRow(true); 784 785 if (this.isSpinnerActive()) { 786 this.stopLoadingSpinnerAnimation(); 787 } 788 789 /* Stop, if there is, the timeout to start the analyzeImages. 790 This is because an image can be set loaded, and the timeout can be set, 791 but this image can be analyzed yet. 792 */ 793 this.stopImgAnalyzerStarter(); 794 795 //On complete callback 796 this.settings.triggerEvent.call(this, isForResize ? 'jg.resize' : 'jg.complete'); 797 this.setGalleryFinalHeight(this.galleryHeightToSet); 798 }; 799 800 /** 801 * Stops any ImgAnalyzer starter (that has an assigned timeout) 802 */ 803 JustifiedGallery.prototype.stopImgAnalyzerStarter = function () { 804 this.yield.flushed = 0; 805 if (this.imgAnalyzerTimeout !== null) { 806 clearTimeout(this.imgAnalyzerTimeout); 807 this.imgAnalyzerTimeout = null; 808 } 809 }; 810 811 /** 812 * Starts the image analyzer. It is not immediately called to let the browser to update the view 813 * 814 * @param isForResize specifies if the image analyzer must be called for resizing or not 815 */ 816 JustifiedGallery.prototype.startImgAnalyzer = function (isForResize) { 817 var that = this; 818 this.stopImgAnalyzerStarter(); 819 this.imgAnalyzerTimeout = setTimeout(function () { 820 that.analyzeImages(isForResize); 821 }, 0.001); // we can't start it immediately due to a IE different behaviour 822 }; 823 824 /** 825 * Checks if the image is loaded or not using another image object. We cannot use the 'complete' image property, 826 * because some browsers, with a 404 set complete = true. 827 * 828 * @param imageSrc the image src to load 829 * @param onLoad callback that is called when the image has been loaded
830 * @param onError callback that is called in case of an error 831 */ 832 JustifiedGallery.prototype.onImageEvent = function (imageSrc, onLoad, onError) { 833 if (!onLoad && !onError) return; 834 835 var memImage = new Image(); 836 var $memImage = $(memImage); 837 if (onLoad) { 838 $memImage.one('load', function () { 839 $memImage.off('load error'); 840 onLoad(memImage); 841 }); 842 } 843 if (onError) { 844 $memImage.one('error', function() { 845 $memImage.off('load error'); 846 onError(memImage); 847 }); 848 } 849 memImage.src = imageSrc; 850 }; 851 852 /** 853 * Init of Justified Gallery controlled 854 * It analyzes all the entries starting theirs loading and calling the image analyzer (that works with loaded images) 855 */ 856 JustifiedGallery.prototype.init = function () { 857 var imagesToLoad = false, skippedImages = false, that = this; 858 $.each(this.entries, function (index, entry) { 859 var $entry = $(entry); 860 var $image = that.imgFromEntry($entry); 861 862 $entry.addClass('jg-entry'); 863 864 if ($entry.data('jg.loaded') !== true && $entry.data('jg.loaded') !== 'skipped') { 865 866 // Link Rel global overwrite 867 if (that.settings.rel !== null) $entry.attr('rel', that.settings.rel); 868 869 // Link Target global overwrite 870 if (that.settings.target !== null) $entry.attr('target', that.settings.target); 871 872 if ($image !== null) { 873 874 // Image src 875 var imageSrc = that.extractImgSrcFromImage($image); 876 $image.attr('src', imageSrc); 877 878 /* If we have the height and the width, we don't wait that the image is loaded, but we start directly 879 * with the justification */ 880 if (that.settings.waitThumbnailsLoad === false) { 881 var width = 0; 882 var height = 0; 883 884 width = parseFloat($image.attr('data-width')); 885 height = parseFloat($image.attr('data-height')); 886 887 if ( isNaN(width) || 0 == width || isNaN(height) || 0 == height ) { 888 width = parseFloat($image.prop('width')); 889 height = parseFloat($image.prop('height')); 890 } 891 892 if ( isNaN(width) || 0 == width || isNaN(height) || 0 == height ) { 893 width = parseFloat($image.attr('width')); 894 height = parseFloat($image.attr('height')); 895 } 896 897 if (!isNaN(width) && !isNaN(height)) { 898 $entry.data('jg.width', width); 899 $entry.data('jg.height', height); 900 $entry.data('jg.loaded', 'skipped'); 901 skippedImages = true; 902 that.startImgAnalyzer(false); 903 return true; // continue 904 } 905 } 906 907 $entry.data('jg.loaded', false); 908 imagesToLoad = true; 909 910 // Spinner start 911 if (!that.isSpinnerActive()) that.startLoadingSpinnerAnimation(); 912 913 that.onImageEvent(imageSrc, function (loadImg) { // image loaded 914 $entry.data('jg.width', loadImg.width);
vendor: 12,401 bytes, lines 915-1216
915 $entry.data('jg.height', loadImg.height); 916 $entry.data('jg.loaded', true); 917 that.startImgAnalyzer(false); 918 }, function () { // image load error 919 $entry.data('jg.loaded', 'error'); 920 that.startImgAnalyzer(false); 921 }); 922 923 } else { 924 $entry.data('jg.loaded', true); 925 $entry.data('jg.width', $entry.width() | parseFloat($entry.css('width')) | 1); 926 $entry.data('jg.height', $entry.height() | parseFloat($entry.css('height')) | 1); 927 } 928 929 } 930 931 }); 932 933 if (!imagesToLoad && !skippedImages) this.startImgAnalyzer(false); 934 this.checkWidth(); 935 }; 936 937 /** 938 * Checks that it is a valid number. If a string is passed it is converted to a number 939 * 940 * @param settingContainer the object that contains the setting (to allow the conversion) 941 * @param settingName the setting name 942 */ 943 JustifiedGallery.prototype.checkOrConvertNumber = function (settingContainer, settingName) { 944 if ($.type(settingContainer[settingName]) === 'string') { 945 settingContainer[settingName] = parseFloat(settingContainer[settingName]); 946 } 947 948 if ($.type(settingContainer[settingName]) === 'number') { 949 if (isNaN(settingContainer[settingName])) throw 'invalid number for ' + settingName; 950 } else { 951 throw settingName + ' must be a number'; 952 } 953 }; 954 955 /** 956 * Checks the sizeRangeSuffixes and, if necessary, converts 957 * its keys from string (e.g. old settings with 'lt100') to int. 958 */ 959 JustifiedGallery.prototype.checkSizeRangesSuffixes = function () { 960 if ($.type(this.settings.sizeRangeSuffixes) !== 'object') { 961 throw 'sizeRangeSuffixes must be defined and must be an object'; 962 } 963 964 var suffixRanges = []; 965 for (var rangeIdx in this.settings.sizeRangeSuffixes) { 966 if (this.settings.sizeRangeSuffixes.hasOwnProperty(rangeIdx)) suffixRanges.push(rangeIdx); 967 } 968 969 var newSizeRngSuffixes = {0: ''}; 970 for (var i = 0; i < suffixRanges.length; i++) { 971 if ($.type(suffixRanges[i]) === 'string') { 972 try { 973 var numIdx = parseInt(suffixRanges[i].replace(/^[a-z]+/, ''), 10); 974 newSizeRngSuffixes[numIdx] = this.settings.sizeRangeSuffixes[suffixRanges[i]]; 975 } catch (e) { 976 throw 'sizeRangeSuffixes keys must contains correct numbers (' + e + ')'; 977 } 978 } else { 979 newSizeRngSuffixes[suffixRanges[i]] = this.settings.sizeRangeSuffixes[suffixRanges[i]]; 980 } 981 } 982 983 this.settings.sizeRangeSuffixes = newSizeRngSuffixes; 984 }; 985 986 /** 987 * check and convert the maxRowHeight setting 988 * requires rowHeight to be already set 989 * TODO: should be always called when only rowHeight is changed 990 * @return number or null 991 */ 992 JustifiedGallery.prototype.retrieveMaxRowHeight = function () { 993 var newMaxRowHeight = null; 994 var rowHeight = this.settings.rowHeight; 995 996 if ($.type(this.settings.maxRowHeight) === 'string') { 997 if (this.settings.maxRowHeight.match(/^[0-9]+%$/)) { 998 newMaxRowHeight = rowHeight * parseFloat(this.settings.maxRowHeight.match(/^([0-9]+)%$/)[1]) / 100; 999 } else { 1000 newMaxRowHeight = parseFloat(this.settings.maxRowHeight); 1001 } 1002 } else if ($.type(this.settings.maxRowHeight) === 'number') { 1003 newMaxRowHeight = this.settings.maxRowHeight; 1004 } else if (this.settings.maxRowHeight === false || this.settings.maxRowHeight == null) { 1005 return null; 1006 } else { 1007 throw 'maxRowHeight must be a number or a percentage'; 1008 } 1009 1010 // check if the converted value is not a number 1011 if (isNaN(newMaxRowHeight)) throw 'invalid number for maxRowHeight'; 1012 1013 // check values, maxRowHeight must be >= rowHeight 1014 if (newMaxRowHeight < rowHeight) newMaxRowHeight = rowHeight; 1015 1016 return newMaxRowHeight; 1017 }; 1018 1019 /** 1020 * Checks the settings 1021 */ 1022 JustifiedGallery.prototype.checkSettings = function () { 1023 this.checkSizeRangesSuffixes(); 1024 1025 this.checkOrConvertNumber(this.settings, 'rowHeight'); 1026 this.checkOrConvertNumber(this.settings, 'margins'); 1027 this.checkOrConvertNumber(this.settings, 'border'); 1028 1029 var lastRowModes = [ 1030 'justify', 1031 'nojustify', 1032 'left', 1033 'center', 1034 'right', 1035 'hide' 1036 ]; 1037 if (lastRowModes.indexOf(this.settings.lastRow) === -1) { 1038 throw 'lastRow must be one of: ' + lastRowModes.join(', '); 1039 } 1040 1041 this.checkOrConvertNumber(this.settings, 'justifyThreshold'); 1042 if (this.settings.justifyThreshold < 0 || this.settings.justifyThreshold > 1) { 1043 throw 'justifyThreshold must be in the interval [0,1]'; 1044 } 1045 if ($.type(this.settings.cssAnimation) !== 'boolean') { 1046 throw 'cssAnimation must be a boolean'; 1047 } 1048 1049 if ($.type(this.settings.captions) !== 'boolean') throw 'captions must be a boolean'; 1050 this.checkOrConvertNumber(this.settings.captionSettings, 'animationDuration'); 1051 1052 this.checkOrConvertNumber(this.settings.captionSettings, 'visibleOpacity'); 1053 if (this.settings.captionSettings.visibleOpacity < 0 || 1054 this.settings.captionSettings.visibleOpacity > 1) { 1055 throw 'captionSettings.visibleOpacity must be in the interval [0, 1]'; 1056 } 1057 1058 this.checkOrConvertNumber(this.settings.captionSettings, 'nonVisibleOpacity'); 1059 if (this.settings.captionSettings.nonVisibleOpacity < 0 || 1060 this.settings.captionSettings.nonVisibleOpacity > 1) { 1061 throw 'captionSettings.nonVisibleOpacity must be in the interval [0, 1]'; 1062 } 1063 1064 this.checkOrConvertNumber(this.settings, 'imagesAnimationDuration'); 1065 this.checkOrConvertNumber(this.settings, 'refreshTime'); 1066 this.checkOrConvertNumber(this.settings, 'refreshSensitivity'); 1067 if ($.type(this.settings.randomize) !== 'boolean') throw 'randomize must be a boolean'; 1068 if ($.type(this.settings.selector) !== 'string') throw 'selector must be a string'; 1069 1070 if (this.settings.sort !== false && !$.isFunction(this.settings.sort)) { 1071 throw 'sort must be false or a comparison function'; 1072 } 1073 1074 if (this.settings.filter !== false && !$.isFunction(this.settings.filter) && 1075 $.type(this.settings.filter) !== 'string') { 1076 throw 'filter must be false, a string or a filter function'; 1077 } 1078 }; 1079 1080 /** 1081 * It brings all the indexes from the sizeRangeSuffixes and it orders them. They are then sorted and returned. 1082 * @returns {Array} sorted suffix ranges 1083 */ 1084 JustifiedGallery.prototype.retrieveSuffixRanges = function () { 1085 var suffixRanges = []; 1086 for (var rangeIdx in this.settings.sizeRangeSuffixes) { 1087 if (this.settings.sizeRangeSuffixes.hasOwnProperty(rangeIdx)) suffixRanges.push(parseInt(rangeIdx, 10)); 1088 } 1089 suffixRanges.sort(function (a, b) { return a > b ? 1 : a < b ? -1 : 0; }); 1090 return suffixRanges; 1091 }; 1092 1093 /** 1094 * Update the existing settings only changing some of them 1095 * 1096 * @param newSettings the new settings (or a subgroup of them) 1097 */ 1098 JustifiedGallery.prototype.updateSettings = function (newSettings) { 1099 // In this case Justified Gallery has been called again changing only some options 1100 this.settings = $.extend({}, this.settings, newSettings); 1101 this.checkSettings(); 1102 1103 // As reported in the settings: negative value = same as margins, 0 = disabled 1104 this.border = this.settings.border >= 0 ? this.settings.border : this.settings.margins; 1105 1106 this.maxRowHeight = this.retrieveMaxRowHeight(); 1107 this.suffixRanges = this.retrieveSuffixRanges(); 1108 }; 1109 1110 JustifiedGallery.prototype.defaults = { 1111 sizeRangeSuffixes: { }, /* e.g. Flickr configuration 1112 { 1113 100: '_t', // used when longest is less than 100px 1114 240: '_m', // used when longest is between 101px and 240px 1115 320: '_n', // ... 1116 500: '', 1117 640: '_z', 1118 1024: '_b' // used as else case because it is the last 1119 } 1120 */ 1121 thumbnailPath: undefined, /* If defined, sizeRangeSuffixes is not used, and this function is used to determine the 1122 path relative to a specific thumbnail size. The function should accept respectively three arguments: 1123 current path, width and height */ 1124 rowHeight: 120, // required? required to be > 0? 1125 maxRowHeight: false, // false or negative value to deactivate. Positive number to express the value in pixels, 1126 // A string '[0-9]+%' to express in percentage (e.g. 300% means that the row height 1127 // can't exceed 3 * rowHeight) 1128 margins: 1, 1129 border: -1, // negative value = same as margins, 0 = disabled, any other value to set the border 1130 1131 lastRow: 'nojustify', // ⦠which is the same as 'left', or can be 'justify', 'center', 'right' or 'hide' 1132 1133 justifyThreshold: 0.90, /* if row width / available space > 0.90 it will be always justified 1134 * (i.e. lastRow setting is not considered) */ 1135 waitThumbnailsLoad: true, 1136 captions: true, 1137 cssAnimation: true, 1138 imagesAnimationDuration: 500, // ignored with css animations 1139 captionSettings: { // ignored with css animations 1140 animationDuration: 500, 1141 visibleOpacity: 0.7, 1142 nonVisibleOpacity: 0.0 1143 }, 1144 rel: null, // rewrite the rel of each analyzed links 1145 target: null, // rewrite the target of all links 1146 extension: /\.[^.\\/]+$/, // regexp to capture the extension of an image 1147 refreshTime: 200, // time interval (in ms) to check if the page changes its width 1148 refreshSensitivity: 0, // change in width allowed (in px) without re-building the gallery 1149 randomize: false, 1150 rtl: false, // right-to-left mode 1151 sort: false, /* 1152 - false: to do not sort 1153 - function: to sort them using the function as comparator (see Array.prototype.sort()) 1154 */ 1155 filter: false, /* 1156 - false, null or undefined: for a disabled filter 1157 - a string: an entry is kept if entry.is(filter string) returns true 1158 see jQuery's .is() function for further information 1159 - a function: invoked with arguments (entry, index, array). Return true to keep the entry, false otherwise. 1160 It follows the specifications of the Array.prototype.filter() function of JavaScript. 1161 */ 1162 selector: 'a, div:not(.spinner)', // The selector that is used to know what are the entries of the gallery 1163 imgSelector: '> img, > a > img', // The selector that is used to know what are the images of each entry 1164 triggerEvent: function (event) { // This is called to trigger events, the default behavior is to call $.trigger 1165 this.$gallery.trigger(event); // Consider that 'this' is this set to the JustifiedGallery object, so it can 1166 } // access to fields such as $gallery, useful to trigger events with jQuery. 1167 }; 1168 1169 /** 1170 * Justified Gallery plugin for jQuery 1171 * 1172 * Events 1173 * - jg.complete : called when all the gallery has been created 1174 * - jg.resize : called when the gallery has been resized 1175 * - jg.rowflush : when a new row appears 1176 * 1177 * @param arg the action (or the settings) passed when the plugin is called 1178 * @returns {*} the object itself 1179 */ 1180 $.fn.justifiedGallery = function (arg) { 1181 return this.each(function (index, gallery) { 1182 1183 var $gallery = $(gallery); 1184 $gallery.addClass('justified-gallery'); 1185 1186 var controller = $gallery.data('jg.controller'); 1187 if (typeof controller === 'undefined') { 1188 // Create controller and assign it to the object data 1189 if (typeof arg !== 'undefined' && arg !== null && $.type(arg) !== 'object') { 1190 if (arg === 'destroy') return; // Just a call to an unexisting object 1191 throw 'The argument must be an object'; 1192 } 1193 controller = new JustifiedGallery($gallery, $.extend({}, JustifiedGallery.prototype.defaults, arg)); 1194 $gallery.data('jg.controller', controller); 1195 } else if (arg === 'norewind') { 1196 // In this case we don't rewind: we analyze only the latest images (e.g. to complete the last unfinished row 1197 // ... left to be more readable 1198 } else if (arg === 'destroy') { 1199 controller.destroy(); 1200 return; 1201 } else { 1202 // In this case Justified Gallery has been called again changing only some options 1203 controller.updateSettings(arg); 1204 controller.rewind(); 1205 } 1206 1207 // Update the entries list 1208 if (!controller.updateEntries(arg === 'norewind')) return; 1209 1210 // Init justified gallery 1211 controller.init(); 1212 1213 }); 1214 }; 1215 1216}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.