PageSourceSearch

https://fongphotography.online/sy-inc/js/justified.js?0d6a07403b8fe83ff73a9dcbd3ef7c0e

js fongphotography.online collected 2026-10-10 01:34:15 UTC 42,484 bytes, 1,171 lines download raw bytes

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.