aboutsummaryrefslogtreecommitdiffstats
path: root/library/justifiedGallery/jquery.justifiedGallery.js
diff options
context:
space:
mode:
authorredmatrix <redmatrix@redmatrix.me>2015-07-02 21:58:28 -0700
committerredmatrix <redmatrix@redmatrix.me>2015-07-02 21:58:28 -0700
commit1a5898bf80246fca87ecd7a56f9b7e4780e4089c (patch)
tree7f80261ecac578fe8cedd1b32c591d37eda86dd8 /library/justifiedGallery/jquery.justifiedGallery.js
parent8b2b92fe3267af0b7f3fea33940a440c847c84f2 (diff)
parent54301a9ff518224309664cd8c2f23cba0d30cb2d (diff)
downloadvolse-hubzilla-1a5898bf80246fca87ecd7a56f9b7e4780e4089c.tar.gz
volse-hubzilla-1a5898bf80246fca87ecd7a56f9b7e4780e4089c.tar.bz2
volse-hubzilla-1a5898bf80246fca87ecd7a56f9b7e4780e4089c.zip
sync abook entries from redmatrix
Merge branch 'master' of https://github.com/redmatrix/redmatrix Conflicts: include/items.php
Diffstat (limited to 'library/justifiedGallery/jquery.justifiedGallery.js')
-rw-r--r--library/justifiedGallery/jquery.justifiedGallery.js1540
1 files changed, 969 insertions, 571 deletions
diff --git a/library/justifiedGallery/jquery.justifiedGallery.js b/library/justifiedGallery/jquery.justifiedGallery.js
index 213a7f286..7c63149a3 100644
--- a/library/justifiedGallery/jquery.justifiedGallery.js
+++ b/library/justifiedGallery/jquery.justifiedGallery.js
@@ -1,699 +1,1097 @@
/*!
- * Justified Gallery - v3.5.4
+ * Justified Gallery - v3.6.0
* http://miromannino.github.io/Justified-Gallery/
* Copyright (c) 2015 Miro Mannino
* Licensed under the MIT license.
*/
(function($) {
- /* Events
- jg.complete : called when all the gallery has been created
- jg.resize : called when the gallery has been resized
- */
-
- $.fn.justifiedGallery = function (arg) {
-
- // Default options
- var defaults = {
- sizeRangeSuffixes : {
- 'lt100': '', // e.g. Flickr uses '_t'
- 'lt240': '', // e.g. Flickr uses '_m'
- 'lt320': '', // e.g. Flickr uses '_n'
- 'lt500': '', // e.g. Flickr uses ''
- 'lt640': '', // e.g. Flickr uses '_z'
- 'lt1024': '', // e.g. Flickr uses '_b'
- },
- rowHeight : 120,
- maxRowHeight : 0, // negative value = no limits, 0 = 1.5 * rowHeight
- margins : 1,
- border: -1, // negative value = same as margins, 0 = disabled
-
- lastRow : 'nojustify', // or can be 'justify' or 'hide'
- justifyThreshold: 0.75, /* if row width / available space > 0.75 it will be always justified
- (i.e. lastRow setting is not considered) */
- fixedHeight : false,
- waitThumbnailsLoad : true,
- captions : true,
- cssAnimation: false,
- imagesAnimationDuration : 500, // ignored with css animations
- captionSettings : { // ignored with css animations
- animationDuration : 500,
- visibleOpacity : 0.7,
- nonVisibleOpacity : 0.0
- },
- rel : null, // rewrite the rel of each analyzed links
- target : null, // rewrite the target of all links
- extension : /\.[^.\\/]+$/,
- refreshTime : 100,
- randomize : false
+ /**
+ * Justified Gallery controller constructor
+ *
+ * @param $gallery the gallery to build
+ * @param settings the settings (the defaults are in $.fn.justifiedGallery.defaults)
+ * @constructor
+ */
+ var JustifiedGallery = function ($gallery, settings) {
+
+ this.settings = settings;
+ this.checkSettings();
+
+ this.imgAnalyzerTimeout = null;
+ this.entries = null;
+ this.buildingRow = {
+ entriesBuff : [],
+ width : 0,
+ aspectRatio : 0
+ };
+ this.lastAnalyzedIndex = -1;
+ this.yield = {
+ every : 2, // do a flush every n flushes (must be greater than 1)
+ flushed : 0 // flushed rows without a yield
};
+ this.border = settings.border >= 0 ? settings.border : settings.margins;
+ this.maxRowHeight = this.retrieveMaxRowHeight();
+ this.suffixRanges = this.retrieveSuffixRanges();
+ this.offY = this.border;
+ this.spinner = {
+ phase : 0,
+ timeSlot : 150,
+ $el : $('<div class="spinner"><span></span><span></span><span></span></div>'),
+ intervalId : null
+ };
+ this.checkWidthIntervalId = null;
+ this.galleryWidth = $gallery.width();
+ this.$gallery = $gallery;
- function getSuffix(width, height, context) {
- var longestSide;
- longestSide = (width > height) ? width : height;
- if (longestSide <= 100) {
- return context.settings.sizeRangeSuffixes.lt100;
- } else if (longestSide <= 240) {
- return context.settings.sizeRangeSuffixes.lt240;
- } else if (longestSide <= 320) {
- return context.settings.sizeRangeSuffixes.lt320;
- } else if (longestSide <= 500) {
- return context.settings.sizeRangeSuffixes.lt500;
- } else if (longestSide <= 640) {
- return context.settings.sizeRangeSuffixes.lt640;
- } else {
- return context.settings.sizeRangeSuffixes.lt1024;
+ };
+
+ /** @returns {String} the best suffix given the width and the height */
+ JustifiedGallery.prototype.getSuffix = function (width, height) {
+ var longestSide, i;
+ longestSide = (width > height) ? width : height;
+ for (i = 0; i < this.suffixRanges.length; i++) {
+ if (longestSide <= this.suffixRanges[i]) {
+ return this.settings.sizeRangeSuffixes[this.suffixRanges[i]];
}
}
+ return this.settings.sizeRangeSuffixes[this.suffixRanges[i - 1]];
+ };
+
+ /**
+ * Remove the suffix from the string
+ *
+ * @returns {string} a new string without the suffix
+ */
+ JustifiedGallery.prototype.removeSuffix = function (str, suffix) {
+ return str.substring(0, str.length - suffix.length);
+ };
- function endsWith(str, suffix) {
- return str.indexOf(suffix, str.length - suffix.length) !== -1;
+ /**
+ * @returns {boolean} a boolean to say if the suffix is contained in the str or not
+ */
+ JustifiedGallery.prototype.endsWith = function (str, suffix) {
+ return str.indexOf(suffix, str.length - suffix.length) !== -1;
+ };
+
+ /**
+ * Get the used suffix of a particular url
+ *
+ * @param str
+ * @returns {String} return the used suffix
+ */
+ JustifiedGallery.prototype.getUsedSuffix = function (str) {
+ for (var si in this.settings.sizeRangeSuffixes) {
+ if (this.settings.sizeRangeSuffixes.hasOwnProperty(si)) {
+ if (this.settings.sizeRangeSuffixes[si].length === 0) continue;
+ if (this.endsWith(str, this.settings.sizeRangeSuffixes[si])) return this.settings.sizeRangeSuffixes[si];
+ }
}
+ return '';
+ };
+
+ /**
+ * Given an image src, with the width and the height, returns the new image src with the
+ * best suffix to show the best quality thumbnail.
+ *
+ * @returns {String} the suffix to use
+ */
+ JustifiedGallery.prototype.newSrc = function (imageSrc, imgWidth, imgHeight) {
+ var matchRes = imageSrc.match(this.settings.extension);
+ var ext = (matchRes != null) ? matchRes[0] : '';
+ var newImageSrc = imageSrc.replace(this.settings.extension, '');
+ newImageSrc = this.removeSuffix(newImageSrc, this.getUsedSuffix(newImageSrc));
+ newImageSrc += this.getSuffix(imgWidth, imgHeight) + ext;
+ return newImageSrc;
+ };
- function removeSuffix(str, suffix) {
- return str.substring(0, str.length - suffix.length);
+ /**
+ * Shows the images that is in the given entry
+ *
+ * @param $entry the entry
+ * @param callback the callback that is called when the show animation is finished
+ */
+ JustifiedGallery.prototype.showImg = function ($entry, callback) {
+ if (this.settings.cssAnimation) {
+ $entry.addClass('entry-visible');
+ if (callback) callback();
+ } else {
+ $entry.stop().fadeTo(this.settings.imagesAnimationDuration, 1.0, callback);
}
+ };
+
+ /**
+ * Extract the image src form the image, looking from the 'safe-src', and if it can't be found, from the
+ * 'src' attribute. It saves in the image data the 'jg.originalSrc' field, with the extracted src.
+ *
+ * @param $image the image to analyze
+ * @returns {String} the extracted src
+ */
+ JustifiedGallery.prototype.extractImgSrcFromImage = function ($image) {
+ var imageSrc = (typeof $image.data('safe-src') !== 'undefined') ? $image.data('safe-src') : $image.attr('src');
+ $image.data('jg.originalSrc', imageSrc);
+ return imageSrc;
+ };
+
+ /** @returns {jQuery} the image in the given entry */
+ JustifiedGallery.prototype.imgFromEntry = function ($entry) {
+ var $img = $entry.find('> img');
+ if ($img.length === 0) $img = $entry.find('> a > img');
+ return $img.length === 0 ? null : $img;
+ };
+
+ /** @returns {jQuery} the caption in the given entry */
+ JustifiedGallery.prototype.captionFromEntry = function ($entry) {
+ var $caption = $entry.find('> .caption');
+ return $caption.length === 0 ? null : $caption;
+ };
- function getUsedSuffix(str, context) {
- var voidSuffix = false;
- for (var si in context.settings.sizeRangeSuffixes) {
- if (context.settings.sizeRangeSuffixes[si].length === 0) {
- voidSuffix = true;
- continue;
+ /**
+ * Display the entry
+ *
+ * @param {jQuery} $entry the entry to display
+ * @param {int} x the x position where the entry must be positioned
+ * @param y the y position where the entry must be positioned
+ * @param imgWidth the image width
+ * @param imgHeight the image height
+ * @param rowHeight the row height of the row that owns the entry
+ */
+ JustifiedGallery.prototype.displayEntry = function ($entry, x, y, imgWidth, imgHeight, rowHeight) {
+ $entry.width(imgWidth);
+ $entry.height(rowHeight);
+ $entry.css('top', y);
+ $entry.css('left', x);
+
+ var $image = this.imgFromEntry($entry);
+ if ($image !== null) {
+ $image.css('width', imgWidth);
+ $image.css('height', imgHeight);
+ $image.css('margin-left', - imgWidth / 2);
+ $image.css('margin-top', - imgHeight / 2);
+
+ // Image reloading for an high quality of thumbnails
+ var imageSrc = $image.attr('src');
+ var newImageSrc = this.newSrc(imageSrc, imgWidth, imgHeight);
+
+ $image.one('error', function () {
+ $image.attr('src', $image.data('jg.originalSrc')); //revert to the original thumbnail, we got it.
+ });
+
+ var loadNewImage = function () {
+ if (imageSrc !== newImageSrc) { //load the new image after the fadeIn
+ $image.attr('src', newImageSrc);
}
- if (endsWith(str, context.settings.sizeRangeSuffixes[si])) {
- return context.settings.sizeRangeSuffixes[si];
+ };
+
+ if ($entry.data('jg.loaded') === 'skipped') {
+ this.onImageEvent(imageSrc, $.proxy(function() {
+ this.showImg($entry, loadNewImage);
+ $entry.data('jg.loaded', true);
+ }, this));
+ } else {
+ this.showImg($entry, loadNewImage);
+ }
+
+ } else {
+ this.showImg($entry);
+ }
+
+ this.displayEntryCaption($entry);
+ };
+
+ /**
+ * Display the entry caption. If the caption element doesn't exists, it creates the caption using the 'alt'
+ * or the 'title' attributes.
+ *
+ * @param {jQuery} $entry the entry to process
+ */
+ JustifiedGallery.prototype.displayEntryCaption = function ($entry) {
+ var $image = this.imgFromEntry($entry);
+ if ($image !== null && this.settings.captions) {
+ var $imgCaption = this.captionFromEntry($entry);
+
+ // Create it if it doesn't exists
+ if ($imgCaption == null) {
+ var caption = $image.attr('alt');
+ if (typeof caption === 'undefined') caption = $entry.attr('title');
+ if (typeof caption !== 'undefined') { // Create only we found something
+ $imgCaption = $('<div class="caption">' + caption + '</div>');
+ $entry.append($imgCaption);
+ $entry.data('jg.createdCaption', true);
}
}
- if (voidSuffix) return "";
- else throw 'unknown suffix for ' + str;
+ // Create events (we check again the $imgCaption because it can be still inexistent)
+ if ($imgCaption !== null) {
+ if (!this.settings.cssAnimation) $imgCaption.stop().fadeTo(0, this.settings.captionSettings.nonVisibleOpacity);
+ this.addCaptionEventsHandlers($entry);
+ }
+ } else {
+ this.removeCaptionEventsHandlers($entry);
}
+ };
- /* Given an image src, with the width and the height, returns the new image src with the
- best suffix to show the best quality thumbnail. */
- function newSrc(imageSrc, imgWidth, imgHeight, context) {
- var matchRes = imageSrc.match(context.settings.extension);
- var ext = (matchRes != null) ? matchRes[0] : '';
- var newImageSrc = imageSrc.replace(context.settings.extension, '');
- newImageSrc = removeSuffix(newImageSrc, getUsedSuffix(newImageSrc, context));
- newImageSrc += getSuffix(imgWidth, imgHeight, context) + ext;
- return newImageSrc;
+ /**
+ * The callback for the event 'mouseenter'. It assumes that the event currentTarget is an entry.
+ * It shows the caption using jQuery (or using CSS if it is configured so)
+ *
+ * @param {Event} eventObject the event object
+ */
+ JustifiedGallery.prototype.onEntryMouseEnterForCaption = function (eventObject) {
+ var $caption = this.captionFromEntry($(eventObject.currentTarget));
+ if (this.settings.cssAnimation) {
+ $caption.addClass('caption-visible').removeClass('caption-hidden');
+ } else {
+ $caption.stop().fadeTo(this.settings.captionSettings.animationDuration,
+ this.settings.captionSettings.visibleOpacity);
}
+ };
- function onEntryMouseEnterForCaption (ev) {
- var $caption = $(ev.currentTarget).find('.caption');
- if (ev.data.settings.cssAnimation) {
- $caption.addClass('caption-visible').removeClass('caption-hidden');
- } else {
- $caption.stop().fadeTo(ev.data.settings.captionSettings.animationDuration,
- ev.data.settings.captionSettings.visibleOpacity);
- }
+ /**
+ * The callback for the event 'mouseleave'. It assumes that the event currentTarget is an entry.
+ * It hides the caption using jQuery (or using CSS if it is configured so)
+ *
+ * @param {Event} eventObject the event object
+ */
+ JustifiedGallery.prototype.onEntryMouseLeaveForCaption = function (eventObject) {
+ var $caption = this.captionFromEntry($(eventObject.currentTarget));
+ if (this.settings.cssAnimation) {
+ $caption.removeClass('caption-visible').removeClass('caption-hidden');
+ } else {
+ $caption.stop().fadeTo(this.settings.captionSettings.animationDuration,
+ this.settings.captionSettings.nonVisibleOpacity);
}
+ };
- function onEntryMouseLeaveForCaption (ev) {
- var $caption = $(ev.currentTarget).find('.caption');
- if (ev.data.settings.cssAnimation) {
- $caption.removeClass('caption-visible').removeClass('caption-hidden');
- } else {
- $caption.stop().fadeTo(ev.data.settings.captionSettings.animationDuration,
- ev.data.settings.captionSettings.nonVisibleOpacity);
- }
+ /**
+ * Add the handlers of the entry for the caption
+ *
+ * @param $entry the entry to modify
+ */
+ JustifiedGallery.prototype.addCaptionEventsHandlers = function ($entry) {
+ var captionMouseEvents = $entry.data('jg.captionMouseEvents');
+ if (typeof captionMouseEvents === 'undefined') {
+ captionMouseEvents = {
+ mouseenter: $.proxy(this.onEntryMouseEnterForCaption, this),
+ mouseleave: $.proxy(this.onEntryMouseLeaveForCaption, this)
+ };
+ $entry.on('mouseenter', undefined, undefined, captionMouseEvents.mouseenter);
+ $entry.on('mouseleave', undefined, undefined, captionMouseEvents.mouseleave);
+ $entry.data('jg.captionMouseEvents', captionMouseEvents);
}
+ };
- function showImg($entry, callback, context) {
- if (context.settings.cssAnimation) {
- $entry.addClass('entry-visible');
- callback();
- } else {
- $entry.stop().fadeTo(context.settings.imagesAnimationDuration, 1.0, callback);
+ /**
+ * Remove the handlers of the entry for the caption
+ *
+ * @param $entry the entry to modify
+ */
+ JustifiedGallery.prototype.removeCaptionEventsHandlers = function ($entry) {
+ var captionMouseEvents = $entry.data('jg.captionMouseEvents');
+ if (typeof captionMouseEvents !== 'undefined') {
+ $entry.off('mouseenter', undefined, captionMouseEvents.mouseenter);
+ $entry.off('mouseleave', undefined, captionMouseEvents.mouseleave);
+ $entry.removeData('jg.captionMouseEvents');
+ }
+ };
+
+ /**
+ * Justify the building row, preparing it to
+ *
+ * @param isLastRow
+ * @returns {*}
+ */
+ JustifiedGallery.prototype.prepareBuildingRow = function (isLastRow) {
+ var i, $entry, imgAspectRatio, newImgW, newImgH, justify = true;
+ var minHeight = 0;
+ var availableWidth = this.galleryWidth - 2 * this.border - (
+ (this.buildingRow.entriesBuff.length - 1) * this.settings.margins);
+ var rowHeight = availableWidth / this.buildingRow.aspectRatio;
+ var justifiable = this.buildingRow.width / availableWidth > this.settings.justifyThreshold;
+
+ //Skip the last row if we can't justify it and the lastRow == 'hide'
+ if (isLastRow && this.settings.lastRow === 'hide' && !justifiable) {
+ for (i = 0; i < this.buildingRow.entriesBuff.length; i++) {
+ $entry = this.buildingRow.entriesBuff[i];
+ if (this.settings.cssAnimation)
+ $entry.removeClass('entry-visible');
+ else
+ $entry.stop().fadeTo(0, 0);
}
+ return -1;
}
- function hideImgImmediately($entry, context) {
- if (context.settings.cssAnimation) {
- $entry.removeClass('entry-visible');
+ // With lastRow = nojustify, justify if is justificable (the images will not become too big)
+ if (isLastRow && !justifiable && this.settings.lastRow === 'nojustify') justify = false;
+
+ for (i = 0; i < this.buildingRow.entriesBuff.length; i++) {
+ $entry = this.buildingRow.entriesBuff[i];
+ imgAspectRatio = $entry.data('jg.width') / $entry.data('jg.height');
+
+ if (justify) {
+ newImgW = (i === this.buildingRow.entriesBuff.length - 1) ? availableWidth : rowHeight * imgAspectRatio;
+ newImgH = rowHeight;
+
+ /* With fixedHeight the newImgH must be greater than rowHeight.
+ In some cases here this is not satisfied (due to the justification).
+ But we comment it, because is better to have a shorter but justified row instead
+ to have a cropped image at the end. */
+ /*if (this.settings.fixedHeight && newImgH < this.settings.rowHeight) {
+ newImgW = this.settings.rowHeight * imgAspectRatio;
+ newImgH = this.settings.rowHeight;
+ }*/
+
} else {
- $entry.stop().fadeTo(0, 0);
+ newImgW = this.settings.rowHeight * imgAspectRatio;
+ newImgH = this.settings.rowHeight;
}
+
+ availableWidth -= Math.round(newImgW);
+ $entry.data('jg.jwidth', Math.round(newImgW));
+ $entry.data('jg.jheight', Math.ceil(newImgH));
+ if (i === 0 || minHeight > newImgH) minHeight = newImgH;
}
- function imgFromEntry($entry) {
- var $img = $entry.find('> img');
- if ($img.length === 0) $img = $entry.find('> a > img');
- return $img;
+ if (this.settings.fixedHeight && minHeight > this.settings.rowHeight)
+ minHeight = this.settings.rowHeight;
+
+ return {minHeight: minHeight, justify: justify};
+ };
+
+ /**
+ * Clear the building row data to be used for a new row
+ */
+ JustifiedGallery.prototype.clearBuildingRow = function () {
+ this.buildingRow.entriesBuff = [];
+ this.buildingRow.aspectRatio = 0;
+ this.buildingRow.width = 0;
+ };
+
+ /**
+ * Flush a row: justify it, modify the gallery height accordingly to the row height
+ *
+ * @param isLastRow
+ */
+ JustifiedGallery.prototype.flushRow = function (isLastRow) {
+ var settings = this.settings;
+ var $entry, minHeight, buildingRowRes, offX = this.border;
+
+ buildingRowRes = this.prepareBuildingRow(isLastRow);
+ minHeight = buildingRowRes.minHeight;
+ if (isLastRow && settings.lastRow === 'hide' && minHeight === -1) {
+ this.clearBuildingRow();
+ return;
}
- function displayEntry($entry, x, y, imgWidth, imgHeight, rowHeight, context) {
- var $image = imgFromEntry($entry);
- $image.css('width', imgWidth);
- $image.css('height', imgHeight);
- //if ($entry.get(0) === $image.parent().get(0)) { // this creates an error in link_around_img test
- $image.css('margin-left', - imgWidth / 2);
- $image.css('margin-top', - imgHeight / 2);
- //}
- $entry.width(imgWidth);
- $entry.height(rowHeight);
- $entry.css('top', y);
- $entry.css('left', x);
+ if (this.maxRowHeight.percentage) {
+ if (this.maxRowHeight.value * settings.rowHeight < minHeight) minHeight = this.maxRowHeight.value * settings.rowHeight;
+ } else {
+ if (this.maxRowHeight.value > 0 && this.maxRowHeight.value < minHeight) minHeight = this.maxRowHeight.value;
+ }
- //DEBUG// console.log('displayEntry (w: ' + $image.width() + ' h: ' + $image.height());
+ for (var i = 0; i < this.buildingRow.entriesBuff.length; i++) {
+ $entry = this.buildingRow.entriesBuff[i];
+ this.displayEntry($entry, offX, this.offY, $entry.data('jg.jwidth'), $entry.data('jg.jheight'), minHeight);
+ offX += $entry.data('jg.jwidth') + settings.margins;
+ }
- // Image reloading for an high quality of thumbnails
- var imageSrc = $image.attr('src');
- var newImageSrc = newSrc(imageSrc, imgWidth, imgHeight, context);
+ //Gallery Height
+ this.$gallery.height(this.offY + minHeight + this.border + (this.isSpinnerActive() ? this.getSpinnerHeight() : 0));
- $image.one('error', function () {
- //DEBUG// console.log('revert the original image');
- $image.attr('src', $image.data('jg.originalSrc')); //revert to the original thumbnail, we got it.
- });
+ if (!isLastRow || (minHeight <= this.settings.rowHeight && buildingRowRes.justify)) {
+ //Ready for a new row
+ this.offY += minHeight + this.settings.margins;
+ this.clearBuildingRow();
+ this.$gallery.trigger('jg.rowflush');
+ }
+ };
- function loadNewImage() {
- if (imageSrc !== newImageSrc) { //load the new image after the fadeIn
- $image.attr('src', newImageSrc);
- }
+ /**
+ * Checks the width of the gallery container, to know if a new justification is needed
+ */
+ JustifiedGallery.prototype.checkWidth = function () {
+ this.checkWidthIntervalId = setInterval($.proxy(function () {
+ var galleryWidth = parseInt(this.$gallery.width(), 10);
+ if (this.galleryWidth !== galleryWidth) {
+ this.galleryWidth = galleryWidth;
+ this.rewind();
+
+ // Restart to analyze
+ this.startImgAnalyzer(true);
}
+ }, this), this.settings.refreshTime);
+ };
+
+ /**
+ * @returns {boolean} a boolean saying if the spinner is active or not
+ */
+ JustifiedGallery.prototype.isSpinnerActive = function () {
+ return this.spinner.intervalId != null;
+ };
+
+ /**
+ * @returns {int} the spinner height
+ */
+ JustifiedGallery.prototype.getSpinnerHeight = function () {
+ return this.spinner.$el.innerHeight();
+ };
+
+ /**
+ * Stops the spinner animation and modify the gallery height to exclude the spinner
+ */
+ JustifiedGallery.prototype.stopLoadingSpinnerAnimation = function () {
+ clearInterval(this.spinner.intervalId);
+ this.spinner.intervalId = null;
+ this.$gallery.height(this.$gallery.height() - this.getSpinnerHeight());
+ this.spinner.$el.detach();
+ };
- if ($image.data('jg.loaded') === 'skipped') {
- onImageEvent(imageSrc, function() {
- showImg($entry, loadNewImage, context);
- $image.data('jg.loaded', true);
- });
+ /**
+ * Starts the spinner animation
+ */
+ JustifiedGallery.prototype.startLoadingSpinnerAnimation = function () {
+ var spinnerContext = this.spinner;
+ var $spinnerPoints = spinnerContext.$el.find('span');
+ clearInterval(spinnerContext.intervalId);
+ this.$gallery.append(spinnerContext.$el);
+ this.$gallery.height(this.offY + this.getSpinnerHeight());
+ spinnerContext.intervalId = setInterval(function () {
+ if (spinnerContext.phase < $spinnerPoints.length) {
+ $spinnerPoints.eq(spinnerContext.phase).fadeTo(spinnerContext.timeSlot, 1);
} else {
- showImg($entry, loadNewImage, context);
+ $spinnerPoints.eq(spinnerContext.phase - $spinnerPoints.length).fadeTo(spinnerContext.timeSlot, 0);
}
+ spinnerContext.phase = (spinnerContext.phase + 1) % ($spinnerPoints.length * 2);
+ }, spinnerContext.timeSlot);
+ };
- // Captions ------------------------------
- var captionMouseEvents = $entry.data('jg.captionMouseEvents');
- if (context.settings.captions === true) {
- var $imgCaption = $entry.find('.caption');
- if ($imgCaption.length === 0) { // Create it if it doesn't exists
- var caption = $image.attr('alt');
- if (typeof caption === 'undefined') caption = $entry.attr('title');
- if (typeof caption !== 'undefined') { // Create only we found something
- $imgCaption = $('<div class="caption">' + caption + '</div>');
- $entry.append($imgCaption);
- }
- }
-
- // Create events (we check again the $imgCaption because it can be still inexistent)
- if ($imgCaption.length !== 0) {
- if (!context.settings.cssAnimation) {
- $imgCaption.stop().fadeTo(context.settings.imagesAnimationDuration,
- context.settings.captionSettings.nonVisibleOpacity);
- }
- if (typeof captionMouseEvents === 'undefined') {
- captionMouseEvents = {
- mouseenter: onEntryMouseEnterForCaption,
- mouseleave: onEntryMouseLeaveForCaption
- };
- $entry.on('mouseenter', undefined, context, captionMouseEvents.mouseenter);
- $entry.on('mouseleave', undefined, context, captionMouseEvents.mouseleave);
- $entry.data('jg.captionMouseEvents', captionMouseEvents);
- }
- }
+ /**
+ * Rewind the image analysis to start from the first entry.
+ */
+ JustifiedGallery.prototype.rewind = function () {
+ this.lastAnalyzedIndex = -1;
+ this.offY = this.border;
+ this.clearBuildingRow();
+ };
+
+ /**
+ * Hide the image of the buildingRow to prevent strange effects when the row will be
+ * re-justified again
+ */
+ JustifiedGallery.prototype.hideBuildingRowImages = function () {
+ for (var i = 0; i < this.buildingRow.entriesBuff.length; i++) {
+ if (this.settings.cssAnimation) {
+ this.buildingRow.entriesBuff[i].removeClass('entry-visible');
} else {
- if (typeof captionMouseEvents !== 'undefined') {
- $entry.off('mouseenter', undefined, context, captionMouseEvents.mouseenter);
- $entry.off('mouseleave', undefined, context, captionMouseEvents.mouseleave);
- $entry.removeData('jg.captionMouseEvents');
- }
+ this.buildingRow.entriesBuff[i].stop().fadeTo(0, 0);
}
+ }
+ };
+ /**
+ * Update the entries searching it from the justified gallery HTML element
+ *
+ * @param norewind if norewind only the new entries will be changed (i.e. randomized, sorted or filtered)
+ * @returns {boolean} true if some entries has been founded
+ */
+ JustifiedGallery.prototype.updateEntries = function (norewind) {
+ this.entries = this.$gallery.find(this.settings.selector).toArray();
+ if (this.entries.length === 0) return false;
+
+ // Filter
+ if (this.settings.filter) {
+ this.modifyEntries(this.filterArray, norewind);
+ } else {
+ this.modifyEntries(this.resetFilters, norewind);
}
- function prepareBuildingRow(context, isLastRow) {
- var settings = context.settings;
- var i, $entry, $image, imgAspectRatio, newImgW, newImgH, justify = true;
- var minHeight = 0;
- var availableWidth = context.galleryWidth - 2 * context.border - (
- (context.buildingRow.entriesBuff.length - 1) * settings.margins);
- var rowHeight = availableWidth / context.buildingRow.aspectRatio;
- var justificable = context.buildingRow.width / availableWidth > settings.justifyThreshold;
-
- //Skip the last row if we can't justify it and the lastRow == 'hide'
- if (isLastRow && settings.lastRow === 'hide' && !justificable) {
- for (i = 0; i < context.buildingRow.entriesBuff.length; i++) {
- $entry = context.buildingRow.entriesBuff[i];
- if (settings.cssAnimation)
- $entry.removeClass('entry-visible');
- else
- $entry.stop().fadeTo(0, 0);
- }
- return -1;
- }
+ // Sort or randomize
+ if ($.isFunction(this.settings.sort)) {
+ this.modifyEntries(this.sortArray, norewind);
+ } else if (this.settings.randomize) {
+ this.modifyEntries(this.shuffleArray, norewind);
+ }
- // With lastRow = nojustify, justify if is justificable (the images will not become too big)
- if (isLastRow && !justificable && settings.lastRow === 'nojustify') justify = false;
+ return true;
+ };
+
+ /**
+ * Apply the entries order to the DOM, iterating the entries and appending the images
+ *
+ * @param entries the entries that has been modified and that must be re-ordered in the DOM
+ */
+ JustifiedGallery.prototype.insertToGallery = function (entries) {
+ var that = this;
+ $.each(entries, function () {
+ $(this).appendTo(that.$gallery);
+ });
+ };
- for (i = 0; i < context.buildingRow.entriesBuff.length; i++) {
- $image = imgFromEntry(context.buildingRow.entriesBuff[i]);
- imgAspectRatio = $image.data('jg.imgw') / $image.data('jg.imgh');
+ /**
+ * Shuffle the array using the Fisher-Yates shuffle algorithm
+ *
+ * @param a the array to shuffle
+ * @return the shuffled array
+ */
+ JustifiedGallery.prototype.shuffleArray = function (a) {
+ var i, j, temp;
+ for (i = a.length - 1; i > 0; i--) {
+ j = Math.floor(Math.random() * (i + 1));
+ temp = a[i];
+ a[i] = a[j];
+ a[j] = temp;
+ }
+ this.insertToGallery(a);
+ return a;
+ };
- if (justify) {
- newImgW = (i === context.buildingRow.entriesBuff.length - 1) ? availableWidth
- : rowHeight * imgAspectRatio;
- newImgH = rowHeight;
+ /**
+ * Sort the array using settings.comparator as comparator
+ *
+ * @param a the array to sort (it is sorted)
+ * @return the sorted array
+ */
+ JustifiedGallery.prototype.sortArray = function (a) {
+ a.sort(this.settings.sort);
+ this.insertToGallery(a);
+ return a;
+ };
- /* With fixedHeight the newImgH must be greater than rowHeight.
- In some cases here this is not satisfied (due to the justification).
- But we comment it, because is better to have a shorter but justified row instead
- to have a cropped image at the end. */
- /*if (settings.fixedHeight && newImgH < settings.rowHeight) {
- newImgW = settings.rowHeight * imgAspectRatio;
- newImgH = settings.rowHeight;
- }*/
+ /**
+ * Reset the filters removing the 'jg-filtered' class from all the entries
+ *
+ * @param a the array to reset
+ */
+ JustifiedGallery.prototype.resetFilters = function (a) {
+ for (var i = 0; i < a.length; i++) $(a[i]).removeClass('jg-filtered');
+ return a;
+ };
+ /**
+ * Filter the entries considering theirs classes (if a string has been passed) or using a function for filtering.
+ *
+ * @param a the array to filter
+ * @return the filtered array
+ */
+ JustifiedGallery.prototype.filterArray = function (a) {
+ var settings = this.settings;
+ if ($.type(settings.filter) === 'string') {
+ // Filter only keeping the entries passed in the string
+ return a.filter(function (el) {
+ var $el = $(el);
+ if ($el.is(settings.filter)) {
+ $el.removeClass('jg-filtered');
+ return true;
} else {
- newImgW = settings.rowHeight * imgAspectRatio;
- newImgH = settings.rowHeight;
+ $el.addClass('jg-filtered');
+ return false;
}
+ });
+ } else if ($.isFunction(settings.filter)) {
+ // Filter using the passed function
+ return a.filter(settings.filter);
+ }
+ };
- availableWidth -= Math.round(newImgW);
- $image.data('jg.jimgw', Math.round(newImgW));
- $image.data('jg.jimgh', Math.ceil(newImgH));
- if (i === 0 || minHeight > newImgH) minHeight = newImgH;
- }
+ /**
+ * Modify the entries. With norewind only the new inserted images will be modified (the ones after lastAnalyzedIndex)
+ *
+ * @param functionToApply the function to call to modify the entries (e.g. sorting, randomization, filtering)
+ * @param norewind specify if the norewind has been called or not
+ */
+ JustifiedGallery.prototype.modifyEntries = function (functionToApply, norewind) {
+ var lastEntries = norewind ?
+ this.entries.splice(this.lastAnalyzedIndex + 1, this.entries.length - this.lastAnalyzedIndex - 1)
+ : this.entries;
+ lastEntries = functionToApply.call(this, lastEntries);
+ this.entries = norewind ? this.entries.concat(lastEntries) : lastEntries;
+ };
- if (settings.fixedHeight && minHeight > settings.rowHeight)
- minHeight = settings.rowHeight;
+ /**
+ * Destroy the Justified Gallery instance.
+ *
+ * It clears all the css properties added in the style attributes. We doesn't backup the original
+ * values for those css attributes, because it costs (performance) and because in general one
+ * shouldn't use the style attribute for an uniform set of images (where we suppose the use of
+ * classes). Creating a backup is also difficult because JG could be called multiple times and
+ * with different style attributes.
+ */
+ JustifiedGallery.prototype.destroy = function () {
+ clearInterval(this.checkWidthIntervalId);
+
+ $.each(this.entries, $.proxy(function(_, entry) {
+ var $entry = $(entry);
+
+ // Reset entry style
+ $entry.css('width', '');
+ $entry.css('height', '');
+ $entry.css('top', '');
+ $entry.css('left', '');
+ $entry.data('jg.loaded', undefined);
+ $entry.removeClass('jg-entry');
+
+ // Reset image style
+ var $img = this.imgFromEntry($entry);
+ $img.css('width', '');
+ $img.css('height', '');
+ $img.css('margin-left', '');
+ $img.css('margin-top', '');
+ $img.attr('src', $img.data('jg.originalSrc'));
+ $img.data('jg.originalSrc', undefined);
+
+ // Remove caption
+ this.removeCaptionEventsHandlers($entry);
+ var $caption = this.captionFromEntry($entry);
+ if ($entry.data('jg.createdCaption')) {
+ // remove also the caption element (if created by jg)
+ $entry.data('jg.createdCaption', undefined);
+ if ($caption != null) $caption.remove();
+ } else {
+ if ($caption != null) $caption.fadeTo(0, 1);
+ }
- return {minHeight: minHeight, justify: justify};
- }
+ }, this));
- function rewind(context) {
- context.lastAnalyzedIndex = -1;
- context.buildingRow.entriesBuff = [];
- context.buildingRow.aspectRatio = 0;
- context.buildingRow.width = 0;
- context.offY = context.border;
- }
+ this.$gallery.css('height', '');
+ this.$gallery.removeClass('justified-gallery');
+ this.$gallery.data('jg.controller', undefined);
+ };
- function flushRow(context, isLastRow) {
- var settings = context.settings;
- var $entry, $image, minHeight, buildingRowRes, offX = context.border;
+ /**
+ * Analyze the images and builds the rows. It returns if it found an image that is not loaded.
+ *
+ * @param isForResize if the image analyzer is called for resizing or not, to call a different callback at the end
+ */
+ JustifiedGallery.prototype.analyzeImages = function (isForResize) {
+ for (var i = this.lastAnalyzedIndex + 1; i < this.entries.length; i++) {
+ var $entry = $(this.entries[i]);
+ if ($entry.data('jg.loaded') === true || $entry.data('jg.loaded') === 'skipped') {
+ var availableWidth = this.galleryWidth - 2 * this.border - (
+ (this.buildingRow.entriesBuff.length - 1) * this.settings.margins);
+ var imgAspectRatio = $entry.data('jg.width') / $entry.data('jg.height');
+ if (availableWidth / (this.buildingRow.aspectRatio + imgAspectRatio) < this.settings.rowHeight) {
+ this.flushRow(false);
+ if(++this.yield.flushed >= this.yield.every) {
+ this.startImgAnalyzer(isForResize);
+ return;
+ }
+ }
- //DEBUG// console.log('flush (isLastRow: ' + isLastRow + ')');
+ this.buildingRow.entriesBuff.push($entry);
+ this.buildingRow.aspectRatio += imgAspectRatio;
+ this.buildingRow.width += imgAspectRatio * this.settings.rowHeight;
+ this.lastAnalyzedIndex = i;
- buildingRowRes = prepareBuildingRow(context, isLastRow);
- minHeight = buildingRowRes.minHeight;
- if (isLastRow && settings.lastRow === 'hide' && minHeight === -1) {
- context.buildingRow.entriesBuff = [];
- context.buildingRow.aspectRatio = 0;
- context.buildingRow.width = 0;
+ } else if ($entry.data('jg.loaded') !== 'error') {
return;
}
+ }
- if (settings.maxRowHeight > 0 && settings.maxRowHeight < minHeight)
- minHeight = settings.maxRowHeight;
- else if (settings.maxRowHeight === 0 && (1.5 * settings.rowHeight) < minHeight)
- minHeight = 1.5 * settings.rowHeight;
-
- for (var i = 0; i < context.buildingRow.entriesBuff.length; i++) {
- $entry = context.buildingRow.entriesBuff[i];
- $image = imgFromEntry($entry);
- displayEntry($entry, offX, context.offY, $image.data('jg.jimgw'),
- $image.data('jg.jimgh'), minHeight, context);
- offX += $image.data('jg.jimgw') + settings.margins;
- }
+ // Last row flush (the row is not full)
+ if (this.buildingRow.entriesBuff.length > 0) this.flushRow(true);
- //Gallery Height
- context.$gallery.height(context.offY + minHeight + context.border +
- (context.spinner.active ? context.spinner.$el.innerHeight() : 0)
- );
+ if (this.isSpinnerActive()) {
+ this.stopLoadingSpinnerAnimation();
+ }
- if (!isLastRow || (minHeight <= context.settings.rowHeight && buildingRowRes.justify)) {
- //Ready for a new row
- context.offY += minHeight + context.settings.margins;
+ /* Stop, if there is, the timeout to start the analyzeImages.
+ This is because an image can be set loaded, and the timeout can be set,
+ but this image can be analyzed yet.
+ */
+ this.stopImgAnalyzerStarter();
- //DEBUG// console.log('minHeight: ' + minHeight + ' offY: ' + context.offY);
+ //On complete callback
+ this.$gallery.trigger(isForResize ? 'jg.resize' : 'jg.complete');
+ };
- context.buildingRow.entriesBuff = []; //clear the array creating a new one
- context.buildingRow.aspectRatio = 0;
- context.buildingRow.width = 0;
- context.$gallery.trigger('jg.rowflush');
- }
+ /**
+ * Stops any ImgAnalyzer starter (that has an assigned timeout)
+ */
+ JustifiedGallery.prototype.stopImgAnalyzerStarter = function () {
+ this.yield.flushed = 0;
+ if (this.imgAnalyzerTimeout !== null) clearTimeout(this.imgAnalyzerTimeout);
+ };
+
+ /**
+ * Starts the image analyzer. It is not immediately called to let the browser to update the view
+ *
+ * @param isForResize specifies if the image analyzer must be called for resizing or not
+ */
+ JustifiedGallery.prototype.startImgAnalyzer = function (isForResize) {
+ var that = this;
+ this.stopImgAnalyzerStarter();
+ this.imgAnalyzerTimeout = setTimeout(function () {
+ that.analyzeImages(isForResize);
+ }, 0.001); // we can't start it immediately due to a IE different behaviour
+ };
+
+ /**
+ * Checks if the image is loaded or not using another image object. We cannot use the 'complete' image property,
+ * because some browsers, with a 404 set complete = true.
+ *
+ * @param imageSrc the image src to load
+ * @param onLoad callback that is called when the image has been loaded
+ * @param onError callback that is called in case of an error
+ */
+ JustifiedGallery.prototype.onImageEvent = function (imageSrc, onLoad, onError) {
+ if (!onLoad && !onError) return;
+
+ var memImage = new Image();
+ var $memImage = $(memImage);
+ if (onLoad) {
+ $memImage.one('load', function () {
+ $memImage.off('load error');
+ onLoad(memImage);
+ });
}
+ if (onError) {
+ $memImage.one('error', function() {
+ $memImage.off('load error');
+ onError(memImage);
+ });
+ }
+ memImage.src = imageSrc;
+ };
- function checkWidth(context) {
- context.checkWidthIntervalId = setInterval(function () {
- var galleryWidth = parseInt(context.$gallery.width(), 10);
- if (context.galleryWidth !== galleryWidth) {
- //DEBUG// console.log("resize. old: " + context.galleryWidth + " new: " + galleryWidth);
-
- context.galleryWidth = galleryWidth;
- rewind(context);
+ /**
+ * Init of Justified Gallery controlled
+ * It analyzes all the entries starting theirs loading and calling the image analyzer (that works with loaded images)
+ */
+ JustifiedGallery.prototype.init = function () {
+ var imagesToLoad = false, skippedImages = false, that = this;
+ $.each(this.entries, function (index, entry) {
+ var $entry = $(entry);
+ var $image = that.imgFromEntry($entry);
- // Restart to analyze
- startImgAnalyzer(context, true);
- }
- }, context.settings.refreshTime);
- }
-
- function startLoadingSpinnerAnimation(spinnerContext) {
- clearInterval(spinnerContext.intervalId);
- spinnerContext.intervalId = setInterval(function () {
- if (spinnerContext.phase < spinnerContext.$points.length)
- spinnerContext.$points.eq(spinnerContext.phase).fadeTo(spinnerContext.timeslot, 1);
- else
- spinnerContext.$points.eq(spinnerContext.phase - spinnerContext.$points.length)
- .fadeTo(spinnerContext.timeslot, 0);
- spinnerContext.phase = (spinnerContext.phase + 1) % (spinnerContext.$points.length * 2);
- }, spinnerContext.timeslot);
- }
-
- function stopLoadingSpinnerAnimation(spinnerContext) {
- clearInterval(spinnerContext.intervalId);
- spinnerContext.intervalId = null;
- }
-
- function stopImgAnalyzerStarter(context) {
- context.yield.flushed = 0;
- if (context.imgAnalyzerTimeout !== null) clearTimeout(context.imgAnalyzerTimeout);
- }
-
- function startImgAnalyzer(context, isForResize) {
- stopImgAnalyzerStarter(context);
- context.imgAnalyzerTimeout = setTimeout(function () {
- analyzeImages(context, isForResize);
- }, 0.001);
- analyzeImages(context, isForResize);
- }
-
- function analyzeImages(context, isForResize) {
-
- /* //DEBUG//
- var rnd = parseInt(Math.random() * 10000, 10);
- console.log('analyzeImages ' + rnd + ' start');
- console.log('images status: ');
- for (var i = 0; i < context.entries.length; i++) {
- var $entry = $(context.entries[i]);
- var $image = imgFromEntry($entry);
- console.log(i + ' (alt: ' + $image.attr('alt') + 'loaded: ' + $image.data('jg.loaded') + ')');
- }*/
-
- /* The first row */
- var settings = context.settings;
- var isLastRow;
-
- for (var i = context.lastAnalyzedIndex + 1; i < context.entries.length; i++) {
- var $entry = $(context.entries[i]);
- var $image = imgFromEntry($entry);
-
- if ($image.data('jg.loaded') === true || $image.data('jg.loaded') === 'skipped') {
- isLastRow = i >= context.entries.length - 1;
-
- var availableWidth = context.galleryWidth - 2 * context.border - (
- (context.buildingRow.entriesBuff.length - 1) * settings.margins);
- var imgAspectRatio = $image.data('jg.imgw') / $image.data('jg.imgh');
- if (availableWidth / (context.buildingRow.aspectRatio + imgAspectRatio) < settings.rowHeight) {
- flushRow(context, isLastRow);
- if(++context.yield.flushed >= context.yield.every) {
- //DEBUG// console.log("yield");
- startImgAnalyzer(context, isForResize);
- return;
- }
- }
+ $entry.addClass('jg-entry');
- context.buildingRow.entriesBuff.push($entry);
- context.buildingRow.aspectRatio += imgAspectRatio;
- context.buildingRow.width += imgAspectRatio * settings.rowHeight;
- context.lastAnalyzedIndex = i;
+ if ($entry.data('jg.loaded') !== true && $entry.data('jg.loaded') !== 'skipped') {
- } else if ($image.data('jg.loaded') !== 'error') {
- return;
- }
- }
+ // Link Rel global overwrite
+ if (that.settings.rel !== null) $entry.attr('rel', that.settings.rel);
- // Last row flush (the row is not full)
- if (context.buildingRow.entriesBuff.length > 0) flushRow(context, true);
+ // Link Target global overwrite
+ if (that.settings.target !== null) $entry.attr('target', that.settings.target);
- if (context.spinner.active) {
- context.spinner.active = false;
- context.$gallery.height(context.$gallery.height() - context.spinner.$el.innerHeight());
- context.spinner.$el.detach();
- stopLoadingSpinnerAnimation(context.spinner);
- }
+ if ($image !== null) {
- /* Stop, if there is, the timeout to start the analyzeImages.
- This is because an image can be set loaded, and the timeout can be set,
- but this image can be analyzed yet.
- */
- stopImgAnalyzerStarter(context);
+ // Image src
+ var imageSrc = that.extractImgSrcFromImage($image);
+ $image.attr('src', imageSrc);
- //On complete callback
- if (!isForResize)
- context.$gallery.trigger('jg.complete');
- else
- context.$gallery.trigger('jg.resize');
+ /* If we have the height and the width, we don't wait that the image is loaded, but we start directly
+ * with the justification */
+ if (that.settings.waitThumbnailsLoad === false) {
+ var width = parseInt($image.attr('width'), 10);
+ var height = parseInt($image.attr('height'), 10);
+ if (!isNaN(width) && !isNaN(height)) {
+ $entry.data('jg.width', width);
+ $entry.data('jg.height', height);
+ $entry.data('jg.loaded', 'skipped');
+ skippedImages = true;
+ that.startImgAnalyzer(false);
+ return true; // continue
+ }
+ }
- //DEBUG// console.log('analyzeImages ' + rnd + ' end');
- }
+ $entry.data('jg.loaded', false);
+ imagesToLoad = true;
- function checkSettings (context) {
- var settings = context.settings;
+ // Spinner start
+ if (!that.isSpinnerActive()) {
+ that.startLoadingSpinnerAnimation();
+ }
- function checkSuffixesRange(range) {
- if (typeof settings.sizeRangeSuffixes[range] !== 'string')
- throw 'sizeRangeSuffixes.' + range + ' must be a string';
- }
+ that.onImageEvent(imageSrc, function (loadImg) { // image loaded
+ $entry.data('jg.width', loadImg.width);
+ $entry.data('jg.height', loadImg.height);
+ $entry.data('jg.loaded', true);
+ that.startImgAnalyzer(false);
+ }, function () { // image load error
+ $entry.data('jg.loaded', 'error');
+ that.startImgAnalyzer(false);
+ });
- function checkOrConvertNumber(parent, settingName) {
- if (typeof parent[settingName] === 'string') {
- parent[settingName] = parseFloat(parent[settingName], 10);
- if (isNaN(parent[settingName])) throw 'invalid number for ' + settingName;
- } else if (typeof parent[settingName] === 'number') {
- if (isNaN(parent[settingName])) throw 'invalid number for ' + settingName;
} else {
- throw settingName + ' must be a number';
+ $entry.data('jg.loaded', true);
+ $entry.data('jg.width', $entry.width() | $entry.css('width') | 1);
+ $entry.data('jg.height', $entry.height() | $entry.css('height') | 1);
}
- }
- if (typeof settings.sizeRangeSuffixes !== 'object')
- throw 'sizeRangeSuffixes must be defined and must be an object';
+ }
- checkSuffixesRange('lt100');
- checkSuffixesRange('lt240');
- checkSuffixesRange('lt320');
- checkSuffixesRange('lt500');
- checkSuffixesRange('lt640');
- checkSuffixesRange('lt1024');
+ });
- checkOrConvertNumber(settings, 'rowHeight');
- checkOrConvertNumber(settings, 'maxRowHeight');
+ if (!imagesToLoad && !skippedImages) this.startImgAnalyzer(false);
+ this.checkWidth();
+ };
- if (settings.maxRowHeight > 0 &&
- settings.maxRowHeight < settings.rowHeight) {
- settings.maxRowHeight = settings.rowHeight;
- }
-
- checkOrConvertNumber(settings, 'margins');
- checkOrConvertNumber(settings, 'border');
-
- if (settings.lastRow !== 'nojustify' &&
- settings.lastRow !== 'justify' &&
- settings.lastRow !== 'hide') {
- throw 'lastRow must be "nojustify", "justify" or "hide"';
- }
+ /**
+ * Checks that it is a valid number. If a string is passed it is converted to a number
+ *
+ * @param settingContainer the object that contains the setting (to allow the conversion)
+ * @param settingName the setting name
+ */
+ JustifiedGallery.prototype.checkOrConvertNumber = function (settingContainer, settingName) {
+ if ($.type(settingContainer[settingName]) === 'string') {
+ settingContainer[settingName] = parseFloat(settingContainer[settingName]);
+ }
- checkOrConvertNumber(settings, 'justifyThreshold');
- if (settings.justifyThreshold < 0 || settings.justifyThreshold > 1)
- throw 'justifyThreshold must be in the interval [0,1]';
- if (typeof settings.cssAnimation !== 'boolean') {
- throw 'cssAnimation must be a boolean';
- }
-
- checkOrConvertNumber(settings.captionSettings, 'animationDuration');
- checkOrConvertNumber(settings, 'imagesAnimationDuration');
+ if ($.type(settingContainer[settingName]) === 'number') {
+ if (isNaN(settingContainer[settingName])) throw 'invalid number for ' + settingName;
+ } else {
+ throw settingName + ' must be a number';
+ }
+ };
- checkOrConvertNumber(settings.captionSettings, 'visibleOpacity');
- if (settings.captionSettings.visibleOpacity < 0 || settings.captionSettings.visibleOpacity > 1)
- throw 'captionSettings.visibleOpacity must be in the interval [0, 1]';
+ /**
+ * Checks the sizeRangeSuffixes and, if necessary, converts
+ * its keys from string (e.g. old settings with 'lt100') to int.
+ */
+ JustifiedGallery.prototype.checkSizeRangesSuffixes = function () {
+ if ($.type(this.settings.sizeRangeSuffixes) !== 'object') {
+ throw 'sizeRangeSuffixes must be defined and must be an object';
+ }
- checkOrConvertNumber(settings.captionSettings, 'nonVisibleOpacity');
- if (settings.captionSettings.visibleOpacity < 0 || settings.captionSettings.visibleOpacity > 1)
- throw 'captionSettings.nonVisibleOpacity must be in the interval [0, 1]';
+ var suffixRanges = [];
+ for (var rangeIdx in this.settings.sizeRangeSuffixes) {
+ if (this.settings.sizeRangeSuffixes.hasOwnProperty(rangeIdx)) suffixRanges.push(rangeIdx);
+ }
- if (typeof settings.fixedHeight !== 'boolean') {
- throw 'fixedHeight must be a boolean';
+ var newSizeRngSuffixes = {0: ''};
+ for (var i = 0; i < suffixRanges.length; i++) {
+ if ($.type(suffixRanges[i]) === 'string') {
+ try {
+ var numIdx = parseInt(suffixRanges[i].replace(/^[a-z]+/, ''), 10);
+ newSizeRngSuffixes[numIdx] = this.settings.sizeRangeSuffixes[suffixRanges[i]];
+ } catch (e) {
+ throw 'sizeRangeSuffixes keys must contains correct numbers (' + e + ')';
+ }
+ } else {
+ newSizeRngSuffixes[suffixRanges[i]] = this.settings.sizeRangeSuffixes[suffixRanges[i]];
}
+ }
- if (typeof settings.captions !== 'boolean') {
- throw 'captions must be a boolean';
- }
+ this.settings.sizeRangeSuffixes = newSizeRngSuffixes;
+ };
- checkOrConvertNumber(settings, 'refreshTime');
+ /**
+ * check and convert the maxRowHeight setting
+ */
+ JustifiedGallery.prototype.retrieveMaxRowHeight = function () {
+ var newMaxRowHeight = { };
- if (typeof settings.randomize !== 'boolean') {
- throw 'randomize must be a boolean';
+ if ($.type(this.settings.maxRowHeight) === 'string') {
+ if (this.settings.maxRowHeight.match(/^[0-9]+%$/)) {
+ newMaxRowHeight.value = parseFloat(this.settings.maxRowHeight.match(/^([0-9])+%$/)[1]) / 100;
+ newMaxRowHeight.percentage = false;
+ } else {
+ newMaxRowHeight.value = parseFloat(this.settings.maxRowHeight);
+ newMaxRowHeight.percentage = true;
}
-
+ } else if ($.type(this.settings.maxRowHeight) === 'number') {
+ newMaxRowHeight.value = this.settings.maxRowHeight;
+ newMaxRowHeight.percentage = false;
+ } else {
+ throw 'maxRowHeight must be a number or a percentage';
}
- function onImageEvent(imageSrc, onLoad, onError) {
- if (!onLoad && !onError) {
- return;
- }
- /* Check if the image is loaded or not using another image object.
- We cannot use the 'complete' image property, because some browsers,
- with a 404 set complete = true */
- var memImage = new Image();
- var $memImage = $(memImage);
- if (onLoad) {
- $memImage.one('load', function () {
- $memImage.off('load error');
- onLoad(memImage);
- });
- }
- if (onError) {
- $memImage.one('error', function() {
- $memImage.off('load error');
- onError(memImage);
- });
+ // check if the converted value is not a number
+ if (isNaN(newMaxRowHeight.value)) throw 'invalid number for maxRowHeight';
+
+ // check values
+ if (newMaxRowHeight.percentage) {
+ if (newMaxRowHeight.value < 100) newMaxRowHeight.value = 100;
+ } else {
+ if (newMaxRowHeight.value > 0 && newMaxRowHeight.value < this.settings.rowHeight) {
+ newMaxRowHeight.value = this.settings.rowHeight;
}
- memImage.src = imageSrc;
}
- return this.each(function (index, gallery) {
+ return newMaxRowHeight;
- var $gallery = $(gallery);
- $gallery.addClass('justified-gallery');
+ };
- var context = $gallery.data('jg.context');
- if (typeof context === 'undefined') {
+ /**
+ * Checks the settings
+ */
+ JustifiedGallery.prototype.checkSettings = function () {
+ this.checkSizeRangesSuffixes();
- if (typeof arg !== 'undefined' && arg !== null && typeof arg !== 'object')
- throw 'The argument must be an object';
+ this.checkOrConvertNumber(this.settings, 'rowHeight');
+ this.checkOrConvertNumber(this.settings, 'margins');
+ this.checkOrConvertNumber(this.settings, 'border');
- // Spinner init
- var $spinner = $('<div class="spinner"><span></span><span></span><span></span></div>');
- var extendedSettings = $.extend({}, defaults, arg);
-
- var border = extendedSettings.border >= 0 ? extendedSettings.border : extendedSettings.margins;
-
- //Context init
- context = {
- settings : extendedSettings,
- imgAnalyzerTimeout : null,
- entries : null,
- buildingRow : {
- entriesBuff : [],
- width : 0,
- aspectRatio : 0
- },
- lastAnalyzedIndex : -1,
- yield : {
- every : 2, /* do a flush every context.yield.every flushes (
- * must be greater than 1, else the analyzeImages will loop */
- flushed : 0 //flushed rows without a yield
- },
- border : border,
- offY : border,
- spinner : {
- active : false,
- phase : 0,
- timeslot : 150,
- $el : $spinner,
- $points : $spinner.find('span'),
- intervalId : null
- },
- checkWidthIntervalId : null,
- galleryWidth : $gallery.width(),
- $gallery : $gallery
- };
-
- $gallery.data('jg.context', context);
+ if (this.settings.lastRow !== 'nojustify' &&
+ this.settings.lastRow !== 'justify' &&
+ this.settings.lastRow !== 'hide') {
+ throw 'lastRow must be "nojustify", "justify" or "hide"';
+ }
- } else if (arg === 'norewind') {
- /* Hide the image of the buildingRow to prevent strange effects when the row will be
- re-justified again */
- for (var i = 0; i < context.buildingRow.entriesBuff.length; i++) {
- hideImgImmediately(context.buildingRow.entriesBuff[i], context);
- }
- // In this case we don't rewind, and analyze all the images
- } else {
- context.settings = $.extend({}, context.settings, arg);
- context.border = context.settings.border >= 0 ? context.settings.border : context.settings.margins;
- rewind(context);
- }
-
- checkSettings(context);
-
- context.entries = $gallery.find('> a, > div:not(.spinner, #page-end)').toArray();
- if (context.entries.length === 0) return;
-
- // Randomize
- if (context.settings.randomize) {
- context.entries.sort(function () { return Math.random() * 2 - 1; });
- $.each(context.entries, function () {
- $(this).appendTo($gallery);
- });
- }
+ this.checkOrConvertNumber(this.settings, 'justifyThreshold');
+ if (this.settings.justifyThreshold < 0 || this.settings.justifyThreshold > 1) {
+ throw 'justifyThreshold must be in the interval [0,1]';
+ }
+ if ($.type(this.settings.cssAnimation) !== 'boolean') {
+ throw 'cssAnimation must be a boolean';
+ }
- var imagesToLoad = false;
- var skippedImages = false;
- $.each(context.entries, function (index, entry) {
- var $entry = $(entry);
- var $image = imgFromEntry($entry);
+ if ($.type(this.settings.captions) !== 'boolean') throw 'captions must be a boolean';
+ this.checkOrConvertNumber(this.settings.captionSettings, 'animationDuration');
- $entry.addClass('jg-entry');
+ this.checkOrConvertNumber(this.settings.captionSettings, 'visibleOpacity');
+ if (this.settings.captionSettings.visibleOpacity < 0 ||
+ this.settings.captionSettings.visibleOpacity > 1) {
+ throw 'captionSettings.visibleOpacity must be in the interval [0, 1]';
+ }
- if ($image.data('jg.loaded') !== true && $image.data('jg.loaded') !== 'skipped') {
+ this.checkOrConvertNumber(this.settings.captionSettings, 'nonVisibleOpacity');
+ if (this.settings.captionSettings.nonVisibleOpacity < 0 ||
+ this.settings.captionSettings.nonVisibleOpacity > 1) {
+ throw 'captionSettings.nonVisibleOpacity must be in the interval [0, 1]';
+ }
- // Link Rel global overwrite
- if (context.settings.rel !== null) $entry.attr('rel', context.settings.rel);
+ if ($.type(this.settings.fixedHeight) !== 'boolean') throw 'fixedHeight must be a boolean';
+ this.checkOrConvertNumber(this.settings, 'imagesAnimationDuration');
+ this.checkOrConvertNumber(this.settings, 'refreshTime');
+ if ($.type(this.settings.randomize) !== 'boolean') throw 'randomize must be a boolean';
+ if ($.type(this.settings.selector) !== 'string') throw 'selector must be a string';
- // Link Target global overwrite
- if (context.settings.target !== null) $entry.attr('target', context.settings.target);
+ if (this.settings.sort !== false && !$.isFunction(this.settings.sort)) {
+ throw 'sort must be false or a comparison function';
+ }
- // Image src
- var imageSrc = (typeof $image.data('safe-src') !== 'undefined') ?
- $image.data('safe-src') : $image.attr('src');
- $image.data('jg.originalSrc', imageSrc);
- $image.attr('src', imageSrc);
+ if (this.settings.filter !== false && !$.isFunction(this.settings.sort) &&
+ $.type(this.settings.filter) !== 'string') {
+ throw 'filter must be false, a string or a filter function';
+ }
+ };
- var width = parseInt($image.attr('width'), 10);
- var height = parseInt($image.attr('height'), 10);
- if(context.settings.waitThumbnailsLoad !== true && !isNaN(width) && !isNaN(height)) {
- $image.data('jg.imgw', width);
- $image.data('jg.imgh', height);
- $image.data('jg.loaded', 'skipped');
- skippedImages = true;
- startImgAnalyzer(context, false);
- return true;
- }
+ /**
+ * It brings all the indexes from the sizeRangeSuffixes and it orders them. They are then sorted and returned.
+ * @returns {Array} sorted suffix ranges
+ */
+ JustifiedGallery.prototype.retrieveSuffixRanges = function () {
+ var suffixRanges = [];
+ for (var rangeIdx in this.settings.sizeRangeSuffixes) {
+ if (this.settings.sizeRangeSuffixes.hasOwnProperty(rangeIdx)) suffixRanges.push(parseInt(rangeIdx, 10));
+ }
+ suffixRanges.sort(function (a, b) { return a > b ? 1 : a < b ? -1 : 0; });
+ return suffixRanges;
+ };
- $image.data('jg.loaded', false);
- imagesToLoad = true;
+ /**
+ * Update the existing settings only changing some of them
+ *
+ * @param newSettings the new settings (or a subgroup of them)
+ */
+ JustifiedGallery.prototype.updateSettings = function (newSettings) {
+ // In this case Justified Gallery has been called again changing only some options
+ this.settings = $.extend({}, this.settings, newSettings);
+ this.checkSettings();
+
+ // As reported in the settings: negative value = same as margins, 0 = disabled
+ this.border = this.settings.border >= 0 ? this.settings.border : this.settings.margins;
+
+ this.maxRowHeight = this.retrieveMaxRowHeight();
+ this.suffixRanges = this.retrieveSuffixRanges();
+ };
- // Spinner start
- if (context.spinner.active === false) {
- context.spinner.active = true;
- $gallery.append(context.spinner.$el);
- $gallery.height(context.offY + context.spinner.$el.innerHeight());
- startLoadingSpinnerAnimation(context.spinner);
- }
+ /**
+ * Justified Gallery plugin for jQuery
+ *
+ * Events
+ * - jg.complete : called when all the gallery has been created
+ * - jg.resize : called when the gallery has been resized
+ * - jg.rowflush : when a new row appears
+ *
+ * @param arg the action (or the settings) passed when the plugin is called
+ * @returns {*} the object itself
+ */
+ $.fn.justifiedGallery = function (arg) {
+ return this.each(function (index, gallery) {
- onImageEvent(imageSrc, function imgLoaded (loadImg) {
- //DEBUG// console.log('img load (alt: ' + $image.attr('alt') + ')');
- $image.data('jg.imgw', loadImg.width);
- $image.data('jg.imgh', loadImg.height);
- $image.data('jg.loaded', true);
- startImgAnalyzer(context, false);
- }, function imgLoadError () {
- //DEBUG// console.log('img error (alt: ' + $image.attr('alt') + ')');
- $image.data('jg.loaded', 'error');
- startImgAnalyzer(context, false);
- });
+ var $gallery = $(gallery);
+ $gallery.addClass('justified-gallery');
+ var controller = $gallery.data('jg.controller');
+ if (typeof controller === 'undefined') {
+ // Create controller and assign it to the object data
+ if (typeof arg !== 'undefined' && arg !== null && $.type(arg) !== 'object') {
+ throw 'The argument must be an object';
}
+ controller = new JustifiedGallery($gallery, $.extend({}, $.fn.justifiedGallery.defaults, arg));
+ $gallery.data('jg.controller', controller);
+ } else if (arg === 'norewind') {
+ // In this case we don't rewind: we analyze only the latest images (e.g. to complete the last unfinished row
+ controller.hideBuildingRowImages();
+ } else if (arg === 'destroy') {
+ controller.destroy();
+ return;
+ } else {
+ // In this case Justified Gallery has been called again changing only some options
+ controller.updateSettings(arg);
+ controller.rewind();
+ }
- });
+ // Update the entries list
+ if (!controller.updateEntries(arg === 'norewind')) return;
+
+ // Init justified gallery
+ controller.init();
- if (!imagesToLoad && !skippedImages) startImgAnalyzer(context, false);
- checkWidth(context);
});
+ };
+ // Default options
+ $.fn.justifiedGallery.defaults = {
+ sizeRangeSuffixes: { }, /* e.g. Flickr configuration
+ {
+ 100: '_t', // used when longest is less than 100px
+ 240: '_m', // used when longest is between 101px and 240px
+ 320: '_n', // ...
+ 500: '',
+ 640: '_z',
+ 1024: '_b' // used as else case because it is the last
+ }
+ */
+ rowHeight: 120,
+ maxRowHeight: '200%', // negative value = no limits, number to express the value in pixels,
+ // '[0-9]+%' to express in percentage (e.g. 200% means that the row height
+ // can't exceed 2 * rowHeight)
+ margins: 1,
+ border: -1, // negative value = same as margins, 0 = disabled, any other value to set the border
+
+ lastRow: 'nojustify', // or can be 'justify' or 'hide'
+ justifyThreshold: 0.75, /* if row width / available space > 0.75 it will be always justified
+ * (i.e. lastRow setting is not considered) */
+ fixedHeight: false,
+ waitThumbnailsLoad: true,
+ captions: true,
+ cssAnimation: false,
+ imagesAnimationDuration: 500, // ignored with css animations
+ captionSettings: { // ignored with css animations
+ animationDuration: 500,
+ visibleOpacity: 0.7,
+ nonVisibleOpacity: 0.0
+ },
+ rel: null, // rewrite the rel of each analyzed links
+ target: null, // rewrite the target of all links
+ extension: /\.[^.\\/]+$/, // regexp to capture the extension of an image
+ refreshTime: 100, // time interval (in ms) to check if the page changes its width
+ randomize: false,
+ sort: false, /*
+ - false: to do not sort
+ - function: to sort them using the function as comparator (see Array.prototype.sort())
+ */
+ filter: false, /*
+ - false: for a disabled filter
+ - a string: an entry is kept if entry.is(filter string) returns true
+ see jQuery's .is() function for further information
+ - a function: invoked with arguments (entry, index, array). Return true to keep the entry, false otherwise.
+ see Array.prototype.filter for further information.
+ */
+ selector: '> a, > div:not(.spinner)' // The selector that is used to know what are the entries of the gallery
};
-
+
}(jQuery));