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