PageSourceSearch

https://dauerblog.de/wp-content/plugins/foogallery/assets/js/foogallery.8546d65f.js?ver=3.3.7

js dauerblog.de collected 2026-10-05 03:42:46 UTC 575,211 bytes, 14,939 lines download raw bytes

1(function($, _){
2
3	/**
4	 * @summary A reference to the jQuery object the plugin is registered with.
5	 * @memberof FooGallery
6	 * @function $
7	 * @type {jQuery}
8	 * @description This is used internally for all jQuery operations to help work around issues where multiple jQuery libraries have been included in a single page.
9	 * @example {@caption The following shows the issue when multiple jQuery's are included in a single page.}{@lang xml}
10	 * <script src="jquery-1.12.4.js"></script>
11	 * <script src="foogallery.js"></script>
12	 * <script src="jquery-2.2.4.js"></script>
13	 * <script>
14	 * 	jQuery(function($){
15	 * 		$(".selector").foogallery(); // => This would throw a TypeError: $(...).foogallery is not a function
16	 * 	});
17	 * </script>
18	 * @example {@caption The reason the above throws an error is that the `$.fn.foogallery` function is registered to the first instance of jQuery in the page however the instance used to create the ready callback and actually try to execute `$(...).foogallery()` is the second. To resolve this issue ideally you would remove the second instance of jQuery however you can use the `FooGallery.$` member to ensure you are always working with the instance of jQuery the plugin was registered with.}{@lang xml}
19	 * <script src="jquery-1.12.4.js"></script>
20	 * <script src="foogallery.js"></script>
21	 * <script src="jquery-2.2.4.js"></script>
22	 * <script>
23	 * 	FooGallery.$(function($){
24	 * 		$(".selector").foogallery(); // => It works!
25	 * 	});
26	 * </script>
27	 */
28	_.$ = $;
29
30	/**
31	 * @summary The jQuery plugin namespace.
32	 * @external "jQuery.fn"
33	 * @see {@link http://learn.jquery.com/plugins/basic-plugin-creation/|How to Create a Basic Plugin | jQuery Learning Center}
34	 */
35
36})(
37	// dependencies
38	jQuery,
39	/**
40	 * @summary The core namespace for the plugin containing all its code.
41	 * @namespace FooGallery
42	 * @description This plugin houses all it's code within a single `FooGallery` global variable to prevent polluting the global namespace and to make accessing its various members simpler.
43	 * @example {@caption As this namespace is registered as a global on the `window` object, it can be accessed using the `window.` prefix.}
44	 * var fg = window.FooGallery;
45	 * @example {@caption Or without it.}
46	 * var fg = FooGallery;
47	 * @example {@caption When using this namespace I would recommend aliasing it to a short variable name such as `fg` or as used internally `_`.}
48	 * // alias the FooGallery namespace
49	 * var _ = FooGallery;
50	 * @example {@caption This is not required but lets us write less code and allows the alias to be minified by compressors like UglifyJS. How you choose to alias the namespace is up to you. You can use the simple `var` method as seen above or supply the namespace as a parameter when creating a new scope as seen below.}
51	 * // create a new scope to work in passing the namespace as a parameter
52	 * (function(_){
53	 *
54	 * 	// use `_.` to access members and methods
55	 *
56	 * })(FooGallery);
57	 */
58	window.FooGallery = window.FooGallery || {}
59);
60"use strict";
61
62function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
63
64/*!
65* FooGallery.utils - Contains common utility methods and classes used in our plugins.
66* @version 1.0.1
67* @link https://github.com/steveush/foo-utils#readme
68* @copyright Steve Usher 2026
69* @license Released under the GPL-3.0 license.
70*/
71
72/**
73 * @file This creates the global FooGallery.utils namespace
74 */
75(function ($) {
76  if (!$) {
77    console.warn('jQuery must be included in the page prior to the FooGallery.utils library.');
78    return;
79  }
80
81  function __exists() {
82    try {
83      return !!window.FooGallery.utils; // does the namespace already exist?
84    } catch (err) {
85      return false;
86    }
87  }
88
89  if (!__exists()) {
90    /**
91     * @summary This namespace contains common utility methods and code shared between our plugins.
92     * @global
93     * @namespace FooGallery.utils
94     * @description This namespace relies on jQuery being included in the page prior to it being loaded.
95     */
96    window.FooGallery.utils = {
97      /**
98       * @summary A reference to the jQuery object the library is registered with.
99       * @memberof FooGallery.utils.
100       * @name $
101       * @type {jQuery}
102       * @description This is used internally for all jQuery operations to help work around issues where multiple jQuery libraries have been included in a single page.
103       * @example {@caption The following shows the issue when multiple jQuery's are included in a single page.}{@lang html}
104       * <script src="jquery-1.12.4.js"></script>
105       * <script src="my-plugin.js"></script>
106       * <script src="jquery-2.2.4.js"></script>
107       * <script>
108       * 	jQuery(function($){
109       * 		$(".selector").myPlugin(); // => This would throw a TypeError: $(...).myPlugin is not a function
110       * 	});
111       * </script>
112       * @example {@caption The reason the above throws an error is that the `$.fn.myPlugin` function is registered to the first instance of jQuery in the page however the instance used to create the ready callback and actually try to execute `$(...).myPlugin()` is the second. To resolve this issue ideally you would remove the second instance of jQuery however you can use the `FooGallery.utils.$` member to ensure you are always working with the instance of jQuery the library was registered with.}{@lang html}
113       * <script src="jquery-1.12.4.js"></script>
114       * <script src="my-plugin.js"></script>
115       * <script src="jquery-2.2.4.js"></script>
116       * <script>
117       * 	FooGallery.utils.$(function($){
118       * 		$(".selector").myPlugin(); // => It works!
119       * 	});
120       * </script>
121       */
122      $: $,
123
124      /**
125       * @summary The version of this library.
126       * @memberof FooGallery.utils.
127       * @name version
128       * @type {string}
129       */
130      version: '1.0.1'
131    };
132  } // at this point there will always be a FooGallery.utils namespace registered to the global scope.
133
134})(jQuery);
135
136(function ($, _) {
137  // only register methods if this version is the current version
138  if (_.version !== '1.0.1') return;
139  /**
140   * @summary Contains common type checking utility methods.
141   * @memberof FooGallery.utils.
142   * @namespace is
143   */
144
145  _.is = {};
146  /**
147   * @summary Checks if the `value` is an array.
148   * @memberof FooGallery.utils.is.
149   * @function array
150   * @param {*} value - The value to check.
151   * @returns {boolean} `true` if the supplied `value` is an array.
152   * @example {@run true}
153   * // alias the FooGallery.utils.is namespace
154   * var _is = FooGallery.utils.is;
155   *
156   * console.log( _is.array( [] ) ); // => true
157   * console.log( _is.array( null ) ); // => false
158   * console.log( _is.array( 123 ) ); // => false
159   * console.log( _is.array( "" ) ); // => false
160   */
161
162  _.is.array = function (value) {
163    return '[object Array]' === Object.prototype.toString.call(value);
164  };
165  /**
166   * @summary Checks if the `value` is a boolean.
167   * @memberof FooGallery.utils.is.
168   * @function boolean
169   * @param {*} value - The value to check.
170   * @returns {boolean} `true` if the supplied `value` is a boolean.
171   * @example {@run true}
172   * // alias the FooGallery.utils.is namespace
173   * var _is = FooGallery.utils.is;
174   *
175   * console.log( _is.boolean( true ) ); // => true
176   * console.log( _is.boolean( false ) ); // => true
177   * console.log( _is.boolean( "true" ) ); // => false
178   * console.log( _is.boolean( "false" ) ); // => false
179   * console.log( _is.boolean( 1 ) ); // => false
180   * console.log( _is.boolean( 0 ) ); // => false
181   */
182
183
184  _.is.boolean = function (value) {
185    return '[object Boolean]' === Object.prototype.toString.call(value);
186  };
187  /**
188   * @summary Checks if the `value` is an element.
189   * @memberof FooGallery.utils.is.
190   * @function element
191   * @param {*} value - The value to check.
192   * @returns {boolean} `true` if the supplied `value` is an element.
193   * @example {@run true}
194   * // alias the FooGallery.utils.is namespace
195   * var _is = FooGallery.utils.is,
196   * 	// create an element to test
197   * 	el = document.createElement("span");
198   *
199   * console.log( _is.element( el ) ); // => true
200   * console.log( _is.element( $(el) ) ); // => false
201   * console.log( _is.element( null ) ); // => false
202   * console.log( _is.element( {} ) ); // => false
203   */
204
205
206  _.is.element = function (value) {
207    return (typeof HTMLElement === "undefined" ? "undefined" : _typeof(HTMLElement)) === 'object' ? value instanceof HTMLElement : !!value && _typeof(value) === 'object' && value.nodeType === 1 && typeof value.nodeName === 'string';
208  };
209  /**
210   * @summary Checks if the `value` is empty.
211   * @memberof FooGallery.utils.is.
212   * @function empty
213   * @param {*} value - The value to check.
214   * @returns {boolean} `true` if the supplied `value` is empty.
215   * @description The following values are considered to be empty by this method:
216   *
217   * <ul><!--
218   * --><li>`""`			- An empty string</li><!--
219   * --><li>`0`			- 0 as an integer</li><!--
220   * --><li>`0.0`		- 0 as a float</li><!--
221   * --><li>`[]`			- An empty array</li><!--
222   * --><li>`{}`			- An empty object</li><!--
223   * --><li>`$()`		- An empty jQuery object</li><!--
224   * --><li>`false`</li><!--
225   * --><li>`null`</li><!--
226   * --><li>`undefined`</li><!--
227   * --></ul>
228   * @example {@run true}
229   * // alias the FooGallery.utils.is namespace
230   * var _is = FooGallery.utils.is;
231   *
232   * console.log( _is.empty( undefined ) ); // => true
233   * console.log( _is.empty( null ) ); // => true
234   * console.log( _is.empty( 0 ) ); // => true
235   * console.log( _is.empty( 0.0 ) ); // => true
236   * console.log( _is.empty( "" ) ); // => true
237   * console.log( _is.empty( [] ) ); // => true
238   * console.log( _is.empty( {} ) ); // => true
239   * console.log( _is.empty( 1 ) ); // => false
240   * console.log( _is.empty( 0.1 ) ); // => false
241   * console.log( _is.empty( "one" ) ); // => false
242   * console.log( _is.empty( ["one"] ) ); // => false
243   * console.log( _is.empty( { "name": "My Object" } ) ); // => false
244   */
245
246
247  _.is.empty = function (value) {
248    if (_.is.undef(value) || value === null) return true;
249    if (_.is.number(value) && value === 0) return true;
250    if (_.is.boolean(value) && value === false) return true;
251    if (_.is.string(value) && value.length === 0) return true;
252    if (_.is.array(value) && value.length === 0) return true;
253    if (_.is.jq(value) && value.length === 0) return true;
254
255    if (_.is.hash(value)) {
256      for (var prop in value) {
257        if (value.hasOwnProperty(prop)) return false;
258      }
259
260      return true;
261    }
262
263    return false;
264  };
265  /**
266   * @summary Checks if the `value` is an error.
267   * @memberof FooGallery.utils.is.
268   * @function error
269   * @param {*} value - The value to check.
270   * @returns {boolean} `true` if the supplied `value` is an error.
271   * @example {@run true}
272   * // alias the FooGallery.utils.is namespace
273   * var _is = FooGallery.utils.is,
274   * 	// create some errors to test
275   * 	err1 = new Error("err1"),
276   * 	err2 = new SyntaxError("err2");
277   *
278   * console.log( _is.error( err1 ) ); // => true
279   * console.log( _is.error( err2 ) ); // => true
280   * console.log( _is.error( null ) ); // => false
281   * console.log( _is.error( 123 ) ); // => false
282   * console.log( _is.error( "" ) ); // => false
283   * console.log( _is.error( {} ) ); // => false
284   * console.log( _is.error( [] ) ); // => false
285   */
286
287
288  _.is.error = function (value) {
289    return '[object Error]' === Object.prototype.toString.call(value);
290  };
291  /**
292   * @summary Checks if the `value` is a function.
293   * @memberof FooGallery.utils.is.
294   * @function fn
295   * @param {*} value - The value to check.
296   * @returns {boolean} `true` if the supplied `value` is a function.
297   * @example {@run true}
298   * // alias the FooGallery.utils.is namespace
299   * var _is = FooGallery.utils.is,
300   * 	// create a function to test
301   * 	func = function(){};
302   *
303   * console.log( _is.fn( func ) ); // => true
304   * console.log( _is.fn( null ) ); // => false
305   * console.log( _is.fn( 123 ) ); // => false
306   * console.log( _is.fn( "" ) ); // => false
307   */
308
309
310  _.is.fn = function (value) {
311    return value === window.alert || '[object Function]' === Object.prototype.toString.call(value);
312  };
313  /**
314   * @summary Checks if the `value` is a hash.
315   * @memberof FooGallery.utils.is.
316   * @function hash
317   * @param {*} value - The value to check.
318   * @returns {boolean} `true` if the supplied `value` is a hash.
319   * @example {@run true}
320   * // alias the FooGallery.utils.is namespace
321   * var _is = FooGallery.utils.is;
322   *
323   * console.log( _is.hash( {"some": "prop"} ) ); // => true
324   * console.log( _is.hash( {} ) ); // => true
325   * console.log( _is.hash( window ) ); // => false
326   * console.log( _is.hash( document ) ); // => false
327   * console.log( _is.hash( "" ) ); // => false
328   * console.log( _is.hash( 123 ) ); // => false
329   */
330
331
332  _.is.hash = function (value) {
333    return _.is.object(value) && value.constructor === Object && !value.nodeType && !value.setInterval;
334  };
335  /**
336   * @summary Checks if the `value` is a jQuery object.
337   * @memberof FooGallery.utils.is.
338   * @function jq
339   * @param {*} value - The value to check.
340   * @returns {boolean} `true` if the supplied `value` is a jQuery object.
341   * @example {@run true}
342   * // alias the FooGallery.utils.is namespace
343   * var _is = FooGallery.utils.is,
344   * 	// create an element to test
345   * 	el = document.createElement("span");
346   *
347   * console.log( _is.jq( $(el) ) ); // => true
348   * console.log( _is.jq( $() ) ); // => true
349   * console.log( _is.jq( el ) ); // => false
350   * console.log( _is.jq( {} ) ); // => false
351   * console.log( _is.jq( null ) ); // => false
352   * console.log( _is.jq( 123 ) ); // => false
353   * console.log( _is.jq( "" ) ); // => false
354   */
355
356
357  _.is.jq = function (value) {
358    return !_.is.undef($) && value instanceof $;
359  };
360  /**
361   * @summary Checks if the `value` is a number.
362   * @memberof FooGallery.utils.is.
363   * @function number
364   * @param {*} value - The value to check.
365   * @returns {boolean}
366   * @example {@run true}
367   * // alias the FooGallery.utils.is namespace
368   * var _is = FooGallery.utils.is;
369   *
370   * console.log( _is.number( 123 ) ); // => true
371   * console.log( _is.number( undefined ) ); // => false
372   * console.log( _is.number( null ) ); // => false
373   * console.log( _is.number( "" ) ); // => false
374   */
375
376
377  _.is.number = function (value) {
378    return '[object Number]' === Object.prototype.toString.call(value) && !isNaN(value);
379  };
380  /**
381   * @summary Checks if the `value` is an object.
382   * @memberof FooGallery.utils.is.
383   * @function object
384   * @param {*} value - The value to check.
385   * @returns {boolean} `true` if the supplied `value` is an object.
386   * @example {@run true}
387   * // alias the FooGallery.utils.is namespace
388   * var _is = FooGallery.utils.is;
389   *
390   * console.log( _is.object( {"some": "prop"} ) ); // => true
391   * console.log( _is.object( {} ) ); // => true
392   * console.log( _is.object( window ) ); // => true
393   * console.log( _is.object( document ) ); // => true
394   * console.log( _is.object( undefined ) ); // => false
395   * console.log( _is.object( null ) ); // => false
396   * console.log( _is.object( "" ) ); // => false
397   * console.log( _is.object( 123 ) ); // => false
398   */
399
400
401  _.is.object = function (value) {
402    return '[object Object]' === Object.prototype.toString.call(value) && !_.is.undef(value) && value !== null;
403  };
404  /**
405   * @summary Checks if the `value` is a promise.
406   * @memberof FooGallery.utils.is.
407   * @function promise
408   * @param {*} value - The object to check.
409   * @returns {boolean} `true` if the supplied `value` is an object.
410   * @description This is a simple check to determine if an object is a jQuery promise object. It simply checks the object has a `then` and `promise` function defined.
411   *
412   * The promise object is created as an object literal inside of `jQuery.Deferred`, it has no prototype, nor any other truly unique properties that could be used to distinguish it.
413   *
414   * This method should be a little more accurate than the internal jQuery one that simply checks for a `promise` function.
415   * @example {@run true}
416   * // alias the FooGallery.utils.is namespace
417   * var _is = FooGallery.utils.is;
418   *
419   * console.log( _is.promise( $.Deferred() ) ); // => true
420   * console.log( _is.promise( {} ) ); // => false
421   * console.log( _is.promise( undefined ) ); // => false
422   * console.log( _is.promise( null ) ); // => false
423   * console.log( _is.promise( "" ) ); // => false
424   * console.log( _is.promise( 123 ) ); // => false
425   */
426
427
428  _.is.promise = function (value) {
429    return _.is.object(value) && _.is.fn(value.then) && _.is.fn(value.promise);
430  };
431  /**
432   * @summary Checks if the `value` is a valid CSS length.
433   * @memberof FooGallery.utils.is.
434   * @function size
435   * @param {*} value - The value to check.
436   * @returns {boolean} `true` if the `value` is a number or CSS length.
437   * @example {@run true}
438   * // alias the FooGallery.utils.is namespace
439   * var _is = FooGallery.utils.is;
440   *
441   * console.log( _is.size( 80 ) ); // => true
442   * console.log( _is.size( "80px" ) ); // => true
443   * console.log( _is.size( "80em" ) ); // => true
444   * console.log( _is.size( "80%" ) ); // => true
445   * console.log( _is.size( {} ) ); // => false
446   * console.log( _is.size( undefined ) ); // => false
447   * console.log( _is.size( null ) ); // => false
448   * console.log( _is.size( "" ) ); // => false
449   * @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/length|&lt;length&gt; - CSS | MDN} for more information on CSS length values.
450   */
451
452
453  _.is.size = function (value) {
454    if (!(_.is.string(value) && !_.is.empty(value)) && !_.is.number(value)) return false;
455    return /^(auto|none|(?:[\d.]*)+?(?:%|px|mm|q|cm|in|pt|pc|em|ex|ch|rem|vh|vw|vmin|vmax)?)$/.test(value);
456  };
457  /**
458   * @summary Checks if the `value` is a string.
459   * @memberof FooGallery.utils.is.
460   * @function string
461   * @param {*} value - The value to check.
462   * @returns {boolean} `true` if the `value` is a string.
463   * @example {@run true}
464   * // alias the FooGallery.utils.is namespace
465   * var _is = FooGallery.utils.is;
466   *
467   * console.log( _is.string( "" ) ); // => true
468   * console.log( _is.string( undefined ) ); // => false
469   * console.log( _is.string( null ) ); // => false
470   * console.log( _is.string( 123 ) ); // => false
471   */
472
473
474  _.is.string = function (value) {
475    return '[object String]' === Object.prototype.toString.call(value);
476  };
477  /**
478   * @summary Checks if the `value` is `undefined`.
479   * @memberof FooGallery.utils.is.
480   * @function undef
481   * @param {*} value - The value to check is undefined.
482   * @returns {boolean} `true` if the supplied `value` is `undefined`.
483   * @example {@run true}
484   * // alias the FooGallery.utils.is namespace
485   * var _is = FooGallery.utils.is;
486   *
487   * console.log( _is.undef( undefined ) ); // => true
488   * console.log( _is.undef( null ) ); // => false
489   * console.log( _is.undef( 123 ) ); // => false
490   * console.log( _is.undef( "" ) ); // => false
491   */
492
493
494  _.is.undef = function (value) {
495    return typeof value === 'undefined';
496  };
497})( // dependencies
498FooGallery.utils.$, FooGallery.utils);
499
500(function ($, _, _is) {
501  // only register methods if this version is the current version
502  if (_.version !== '1.0.1') return;
503  /**
504   * @memberof FooGallery.utils.
505   * @namespace fn
506   * @summary Contains common function utility methods.
507   */
508
509  _.fn = {};
510  var fnStr = Function.prototype.toString;
511  /**
512   * @summary The regular expression to test if a function uses the `this._super` method applied by the {@link FooGallery.utils.fn.add} method.
513   * @memberof FooGallery.utils.fn.
514   * @name CONTAINS_SUPER
515   * @type {RegExp}
516   * @default /\b_super\b/
517   * @readonly
518   * @description When the script is first loaded into the page this performs a quick check to see if the browser supports function decompilation. If it does the regular expression is set to match the expected `_super`, however if  function decompilation is not supported, the regular expression is set to match anything effectively making the test always return `true`.
519   * @example {@run true}
520   * // alias the FooGallery.utils.fn namespace
521   * var _fn = FooGallery.utils.fn;
522   *
523   * // create some functions to test
524   * function testFn1(){}
525   * function testFn2(){
526   * 	this._super();
527   * }
528   *
529   * console.log( _fn.CONTAINS_SUPER.test( testFn1 ) ); // => false
530   * console.log( _fn.CONTAINS_SUPER.test( testFn2 ) ); // => true
531   *
532   * // NOTE: in browsers that don't support functional decompilation both tests will return `true`
533   */
534
535  _.fn.CONTAINS_SUPER = /xyz/.test(fnStr.call(function () {
536    //noinspection JSUnresolvedVariable,BadExpressionStatementJS
537    xyz;
538  })) ? /\b_super\b/ : /.*/;
539  /**
540   * @summary An empty function that does nothing. Useful for setting a default value and checking if it has changed.
541   * @memberof FooGallery.utils.fn.
542   * @function noop
543   */
544
545  _.fn.noop = function () {};
546  /**
547   * @summary Adds or overrides the given method `name` on the `proto` using the supplied `fn`.
548   * @memberof FooGallery.utils.fn.
549   * @function addOrOverride
550   * @param {Object} proto - The prototype to add the method to.
551   * @param {string} name - The name of the method to add, if this already exists the original will be exposed within the scope of the supplied `fn` as `this._super`.
552   * @param {function} fn - The function to add to the prototype, if this is overriding an existing method you can use `this._super` to access the original within its' scope.
553   * @description If the new method overrides a pre-existing one, this function will expose the overridden method as `this._super` within the new methods scope.
554   *
555   * This replaces having to write out the following to override a method and call its original:
556   *
557   * ```javascript
558   * var original = MyClass.prototype.someMethod;
559   * MyClass.prototype.someMethod = function(arg1, arg2){
560   * 	// execute the original
561   * 	original.call(this, arg1, arg2);
562   * };
563   * ```
564   *
565   * With the following:
566   *
567   * ```javascript
568   * FooGallery.utils.fn.addOrOverride( MyClass.prototype, "someMethod", function(arg1, arg2){
569   * 	// execute the original
570   * 	this._super(arg1, arg2);
571   * });
572   * ```
573   *
574   * This method is used by the {@link FooGallery.utils.Class} to implement the inheritance of individual methods.
575   * @example {@run true}
576   * // alias the FooGallery.utils.fn namespace
577   * var _fn = FooGallery.utils.fn;
578   *
579   * var proto = {
580   * 	write: function( message ){
581   * 		console.log( "Original#write: " + message );
582   * 	}
583   * };
584   *
585   * proto.write( "My message" ); // => "Original#write: My message"
586   *
587   * _fn.addOrOverride( proto, "write", function( message ){
588   * 	message = "Override#write: " + message;
589   * 	this._super( message );
590   * } );
591   *
592   * proto.write( "My message" ); // => "Original#write: Override#write: My message"
593   */
594
595
596  _.fn.addOrOverride = function (proto, name, fn) {
597    if (!_is.object(proto) || !_is.string(name) || _is.empty(name) || !_is.fn(fn)) return;
598
599    var _super = proto[name],
600        wrap = _is.fn(_super) && _.fn.CONTAINS_SUPER.test(fnStr.call(fn)); // only wrap the function if it overrides a method and makes use of `_super` within it's body.
601
602
603    proto[name] = wrap ? function (_super, fn) {
604      // create a new wrapped that exposes the original method as `_super`
605      return function () {
606        var tmp = this._super;
607        this._super = _super;
608        var ret = fn.apply(this, arguments);
609        this._super = tmp;
610        return ret;
611      };
612    }(_super, fn) : fn;
613  };
614  /**
615   * @summary Exposes the `methods` from the `source` on the `target`.
616   * @memberof FooGallery.utils.fn.
617   * @function expose
618   * @param {Object} source - The object to expose methods from.
619   * @param {Object} target - The object to expose methods on.
620   * @param {String[]}
620 methods - An array of method names to expose.
621   * @param {*} [thisArg] - The value of `this` within the exposed `methods`. Defaults to the `source` object.
622   */
623
624
625  _.fn.expose = function (source, target, methods, thisArg) {
626    if (_is.object(source) && _is.object(target) && _is.array(methods)) {
627      thisArg = _is.undef(thisArg) ? source : thisArg;
628      methods.forEach(function (method) {
629        if (_is.string(method) && _is.fn(source[method])) {
630          target[method] = source[method].bind(thisArg);
631        }
632      });
633    }
634  };
635  /**
636   * @summary Use the `Function.prototype.apply` method on a class constructor using the `new` keyword.
637   * @memberof FooGallery.utils.fn.
638   * @function apply
639   * @param {Object} klass - The class to create.
640   * @param {Array} [args=[]] - The arguments to pass to the constructor.
641   * @returns {Object} The new instance of the `klass` created with the supplied `args`.
642   * @description When using the default `Function.prototype.apply` you can't use it on class constructors requiring the `new` keyword, this method allows us to do that.
643   * @example {@run true}
644   * // alias the FooGallery.utils.fn namespace
645   * var _fn = FooGallery.utils.fn;
646   *
647   * // create a class to test with
648   * function Test( name, value ){
649   * 	if ( !( this instanceof Test )){
650   * 		console.log( "Test instantiated without the `new` keyword." );
651   * 		return;
652   * 	}
653   * 	console.log( "Test: name = " + name + ", value = " + value );
654   * }
655   *
656   * Test.apply( Test, ["My name", "My value"] ); // => "Test instantiated without the `new` keyword."
657   * _fn.apply( Test, ["My name", "My value"] ); // => "Test: name = My name, value = My value"
658   */
659
660
661  _.fn.apply = function (klass, args) {
662    args.unshift(klass);
663    return new (Function.prototype.bind.apply(klass, args))();
664  };
665  /**
666   * @summary Converts the default `arguments` object into a proper array.
667   * @memberof FooGallery.utils.fn.
668   * @function arg2arr
669   * @param {IArguments} args - The arguments object to create an array from.
670   * @returns {Array}
671   * @description This method is simply a replacement for calling `Array.prototype.slice.call()` to create an array from an `arguments` object.
672   * @example {@run true}
673   * // alias the FooGallery.utils.fn namespace
674   * var _fn = FooGallery.utils.fn;
675   *
676   * function callMe(){
677   * 	var args = _fn.arg2arr(arguments);
678   * 	console.log( arguments instanceof Array ); // => false
679   * 	console.log( args instanceof Array ); // => true
680   * 	console.log( args ); // => [ "arg1", "arg2" ]
681   * }
682   *
683   * callMe("arg1", "arg2");
684   */
685
686
687  _.fn.arg2arr = function (args) {
688    return Array.prototype.slice.call(args);
689  };
690  /**
691   * @summary Debounce the `fn` by the supplied `time`.
692   * @memberof FooGallery.utils.fn.
693   * @function debounce
694   * @param {function} fn - The function to debounce.
695   * @param {number} time - The time in milliseconds to delay execution.
696   * @returns {function}
697   * @description This returns a wrapped version of the `fn` which delays its' execution by the supplied `time`. Additional calls to the function will extend the delay until the `time` expires.
698   */
699
700
701  _.fn.debounce = function (fn, time) {
702    var timeout;
703    return function () {
704      var ctx = this,
705          args = _.fn.arg2arr(arguments);
706
707      clearTimeout(timeout);
708      timeout = setTimeout(function () {
709        fn.apply(ctx, args);
710      }, time);
711    };
712  };
713  /**
714   * @summary Throttles the `fn` by the supplied `time`.
715   * @memberof FooGallery.utils.fn.
716   * @function throttle
717   * @param {function} fn - The function to throttle.
718   * @param {number} time - The time in milliseconds to delay execution.
719   * @returns {function}
720   * @description This returns a wrapped version of the `fn` which ensures it's executed only once every `time` milliseconds. The first call to the function will be executed, after that only the last of any additional calls will be executed once the `time` expires.
721   */
722
723
724  _.fn.throttle = function (fn, time) {
725    var last, timeout;
726    return function () {
727      var ctx = this,
728          args = _.fn.arg2arr(arguments);
729
730      if (!last) {
731        fn.apply(ctx, args);
732        last = Date.now();
733      } else {
734        clearTimeout(timeout);
735        timeout = setTimeout(function () {
736          if (Date.now() - last >= time) {
737            fn.apply(ctx, args);
738            last = Date.now();
739          }
740        }, time - (Date.now() - last));
741      }
742    };
743  };
744  /**
745   * @summary A resolved promise object.
746   * @memberof FooGallery.utils.fn.
747   * @name resolved
748   * @type {Promise}
749   */
750
751
752  _.fn.resolved = $.Deferred().resolve().promise();
753  /**
754   * @summary A rejected promise object.
755   * @memberof FooGallery.utils.fn.
756   * @name rejected
757   * @type {Promise}
758   */
759
760  _.fn.rejected = $.Deferred().reject().promise();
761  /**
762   * @summary Return a promise rejected using the supplied args.
763   * @memberof FooGallery.utils.fn.
764   * @function reject
765   * @param {*} [arg1] - The first argument to reject the promise with.
766   * @param {...*} [argN] - Any additional arguments to reject the promise with.
767   * @returns {Promise}
768   */
769
770  _.fn.reject = function (arg1, argN) {
771    var def = $.Deferred(),
772        args = _.fn.arg2arr(arguments);
773
774    return def.reject.apply(def, args).promise();
775  };
776  /**
777   * @summary Return a promise resolved using the supplied args.
778   * @memberof FooGallery.utils.fn.
779   * @function resolve
780   * @param {*} [arg1] - The first argument to resolve the promise with.
781   * @param {...*} [argN] - Any additional arguments to resolve the promise with.
782   * @returns {Promise}
783   */
784
785
786  _.fn.resolve = function (arg1, argN) {
787    var def = $.Deferred(),
788        args = _.fn.arg2arr(arguments);
789
790    return def.resolve.apply(def, args).promise();
791  };
792  /**
793   * @summary Return a promise rejected using the supplied args.
794   * @memberof FooGallery.utils.fn.
795   * @function rejectWith
796   * @param {*} thisArg - The value of `this` within the promises callbacks.
797   * @param {*} [arg1] - The first argument to reject the promise with.
798   * @param {...*} [argN] - Any additional arguments to reject the promise with.
799   * @returns {Promise}
800   */
801
802
803  _.fn.rejectWith = function (thisArg, arg1, argN) {
804    var def = $.Deferred(),
805        args = _.fn.arg2arr(arguments);
806
807    args.shift(); // remove the thisArg
808
809    return def.rejectWith(thisArg, args).promise();
810  };
811  /**
812   * @summary Return a promise resolved using the supplied args.
813   * @memberof FooGallery.utils.fn.
814   * @function resolveWith
815   * @param {*} thisArg - The value of `this` within the promises callbacks.
816   * @param {*} [arg1] - The first argument to resolve the promise with.
817   * @param {...*} [argN] - Any additional arguments to resolve the promise with.
818   * @returns {Promise}
819   */
820
821
822  _.fn.resolveWith = function (thisArg, arg1, argN) {
823    var def = $.Deferred(),
824        args = _.fn.arg2arr(arguments);
825
826    args.shift(); // remove the thisArg
827
828    return def.resolveWith(thisArg, args).promise();
829  };
830  /**
831   * @summary Waits for all promises to complete before resolving with an array containing the return value of each. This method will reject immediately with the first rejection message or error.
832   * @memberof FooGallery.utils.fn.
833   * @function all
834   * @param {Promise[]} promises - The array of promises to wait for.
835   * @returns {Promise}
836   */
837
838
839  _.fn.all = function (promises) {
840    var d = $.Deferred(),
841        results = [];
842
843    if (_is.array(promises) && promises.length > 0) {
844      (function () {
845        /**
846         * Pushes the arguments into the results array at the supplied index.
847         * @ignore
848         * @param {number} index
849         * @param {Array} args
850         */
851        var pushResult = function pushResult(index, args) {
852          if (rejected) return;
853          results[index] = args.length === 0 ? undefined : args.length === 1 ? args[0] : args;
854          remaining--;
855          if (!remaining) d.resolve(results);
856        };
857
858        var remaining = promises.length,
859            rejected = false;
860        var i = 0,
861            l = promises.length;
862
863        var _loop = function _loop() {
864          if (rejected) return "break";
865          var j = i; // hold a scoped reference that can be used in the async callbacks
866
867          if (_is.promise(promises[j])) {
868            promises[j].then(function () {
869              pushResult(j, _.fn.arg2arr(arguments));
870            }, function () {
871              if (rejected) return;
872              rejected = true;
873              d.reject.apply(d, _.fn.arg2arr(arguments));
874            });
875          } else {
876            // if we were supplied something that was not a promise then just add it as a fulfilled result
877            pushResult(j, [promises[j]]);
878          }
879        };
880
881        for (; i < l; i++) {
882          var _ret = _loop();
883
884          if (_ret === "break") break;
885        }
886      })();
887    } else {
888      d.resolve(results);
889    }
890
891    return d.promise();
892  };
893  /**
894   * @summary Waits for all promises to complete before resolving with an array containing the outcome of each.
895   * @memberof FooGallery.utils.fn.
896   * @function allSettled
897   * @param {Promise[]} promises - The array of promises to wait for.
898   * @returns {Promise}
899   */
900
901
902  _.fn.allSettled = function (promises) {
903    var d = $.Deferred(),
904        results = [];
905
906    if (_is.array(promises) && promises.length > 0) {
907      (function () {
908        /**
909         * Sets the value in the results array using the status and args.
910         * @ignore
911         * @param {number} index
912         * @param {string} status
913         * @param {Array} args
914         */
915        var setResult = function setResult(index, status, args) {
916          results[index] = {
917            status: status
918          };
919
920          if (args.length > 0) {
921            var prop = status === "rejected" ? "reason" : "value";
922            results[index][prop] = args.length === 1 ? args[0] : args;
923          }
924
925          remaining--;
926          if (!remaining) d.resolve(results);
927        };
928
929        var remaining = promises.length;
930        var i = 0,
931            l = promises.length;
932
933        var _loop2 = function _loop2() {
934          var j = i; // hold a scoped reference that can be used in the async callbacks
935
936          if (_is.promise(promises[j])) {
937            promises[j].then(function () {
938              setResult(j, "fulfilled", _.fn.arg2arr(arguments));
939            }, function () {
940              setResult(j, "rejected", _.fn.arg2arr(arguments));
941            });
942          } else {
943            // if we were supplied something that was not a promise then just add it as a fulfilled result
944            setResult(j, "fulfilled", [promises[j]]);
945          }
946        };
947
948        for (; i < l; i++) {
949          _loop2();
950        }
951      })();
952    } else {
953      d.resolve(results);
954    }
955
956    return d.promise();
957  };
958})( // dependencies
959FooGallery.utils.$, FooGallery.utils, FooGallery.utils.is);
960
961(function (_, _is) {
962  // only register methods if this version is the current version
963  if (_.version !== '1.0.1') return;
964  /**
965   * @summary Contains common url utility methods.
966   * @memberof FooGallery.utils.
967   * @namespace url
968   */
969
970  _.url = {}; // used for parsing a url into it's parts.
971
972  var _a = document.createElement('a');
973  /**
974   * @summary Parses the supplied url into an object containing it's component parts.
975   * @memberof FooGallery.utils.url.
976   * @function parts
977   * @param {string} url - The url to parse.
978   * @returns {FooGallery.utils.url~Parts}
979   * @example {@run true}
980   * // alias the FooGallery.utils.url namespace
981   * var _url = FooGallery.utils.url;
982   *
983   * console.log( _url.parts( "http://example.com/path/?param=true#something" ) ); // => {"hash":"#something", ...}
984   */
985
986
987  _.url.parts = function (url) {
988    _a.href = url;
989    var port = _a.port ? _a.port : ["http:", "https:"].indexOf(_a.protocol) !== -1 ? _a.protocol === "https:" ? "443" : "80" : "",
990        host = _a.hostname + (port ? ":" + port : ""),
991        origin = _a.origin ? _a.origin : _a.protocol + "//" + host,
992        pathname = _a.pathname.slice(0, 1) === "/" ? _a.pathname : "/" + _a.pathname;
993    return {
994      hash: _a.hash,
995      host: host,
996      hostname: _a.hostname,
997      href: _a.href,
998      origin: origin,
999      pathname: pathname,
1000      port: port,
1001      protocol: _a.protocol,
1002      search: _a.search
1003    };
1004  };
1005  /**
1006   * @summary Given a <code>url</code> that could be relative or full this ensures a full url is returned.
1007   * @memberof FooGallery.utils.url.
1008   * @function full
1009   * @param {string} url - The url to ensure is full.
1010   * @returns {?string} `null` if the given `path` is not a string or empty.
1011   * @description Given a full url this will simply return it however if given a relative url this will create a full url using the current location to fill in the blanks.
1012   * @example {@run true}
1013   * // alias the FooGallery.utils.url namespace
1014   * var _url = FooGallery.utils.url;
1015   *
1016   * console.log( _url.full( "http://example.com/path/" ) ); // => "http://example.com/path/"
1017   * console.log( _url.full( "/path/" ) ); // => "{protocol}//{host}/path/"
1018   * console.log( _url.full( "path/" ) ); // => "{protocol}//{host}/{pathname}/path/"
1019   * console.log( _url.full( "../path/" ) ); // => "{protocol}//{host}/{calculated pathname}/path/"
1020   * console.log( _url.full() ); // => null
1021   * console.log( _url.full( 123 ) ); // => null
1022   */
1023
1024
1025  _.url.full = function (url) {
1026    if (!_is.string(url) || _is.empty(url)) return null;
1027    _a.href = url;
1028    return _a.href;
1029  };
1030  /**
1031   * @summary Gets or sets a parameter in the given <code>search</code> string.
1032   * @memberof FooGallery.utils.url.
1033   * @function param
1034   * @param {string} search - The search string to use (usually `location.search`).
1035   * @param {string} key - The key of the parameter.
1036   * @param {?string} [value] - The value to set for the parameter. If not provided the current value for the `key` is returned.
1037   * @returns {?string} The value of the `key` in the given `search` string if no `value` is supplied or `null` if the `key` does not exist.
1038   * @returns {string} A modified `search` string if a `value` is supplied.
1039   * @example <caption>Shows how to retrieve a parameter value from a search string.</caption>{@run true}
1040   * // alias the FooGallery.utils.url namespace
1041   * var _url = FooGallery.utils.url,
1042   * 	// create a search string to test
1043   * 	search = "?wmode=opaque&autoplay=1";
1044   *
1045   * console.log( _url.param( search, "wmode" ) ); // => "opaque"
1046   * console.log( _url.param( search, "autoplay" ) ); // => "1"
1047   * console.log( _url.param( search, "nonexistent" ) ); // => null
1048   * @example <caption>Shows how to set a parameter value in the given search string.</caption>{@run true}
1049   * // alias the FooGallery.utils.url namespace
1050   * var _url = FooGallery.utils.url,
1051   * 	// create a search string to test
1052   * 	search = "?wmode=opaque&autoplay=1";
1053   *
1054   * console.log( _url.param( search, "wmode", "window" ) ); // => "?wmode=window&autoplay=1"
1055   * console.log( _url.param( search, "autoplay", "0" ) ); // => "?wmode=opaque&autoplay=0"
1056   * console.log( _url.param( search, "v", "2" ) ); // => "?wmode=opaque&autoplay=1&v=2"
1057   */
1058
1059
1060  _.url.param = function (search, key, value) {
1061    if (!_is.string(search) || !_is.string(key) || _is.empty(key)) return search;
1062    var regex, match, result, param;
1063
1064    if (_is.undef(value)) {
1065      regex = new RegExp('[?|&]' + key + '=([^&;]+?)(&|#|;|$)'); // regex to match the key and it's value but only capture the value
1066
1067      match = regex.exec(search) || ["", ""]; // match the param otherwise return an empty string match
1068
1069      result = match[1].replace(/\+/g, '%20'); // replace any + character's with spaces
1070
1071      return _is.string(result) && !_is.empty(result) ? decodeURIComponent(result) : null; // decode the result otherwise return null
1072    }
1073
1074    if (_is.empty(value)) {
1075      regex = new RegExp('^([^#]*\?)(([^#]*)&)?' + key + '(\=[^&#]*)?(&|#|$)');
1076      result = search.replace(regex, '$1$3$5').replace(/^([^#]*)((\?)&|\?(#|$))/, '$1$3$4');
1077    } else {
1078      regex = new RegExp('([?&])' + key + '[^&]*'); // regex to match the key and it's current value but only capture the preceding ? or & char
1079
1080      param = key + '=' + encodeURIComponent(value);
1081      result = search.replace(regex, '$1' + param); // replace any existing instance of the key with the new value
1082      // If nothing was replaced, then add the new param to the end
1083
1084      if (result === search && !regex.test(result)) {
1085        // if no replacement occurred and the parameter is not currently in the result then add it
1086        result += (result.indexOf("?") !== -1 ? '&' : '?') + param;
1087      }
1088    }
1089
1090    return result;
1091  }; //######################
1092  //## Type Definitions ##
1093  //######################
1094
1095  /**
1096   * @summary A plain JavaScript object returned by the {@link FooGallery.utils.url.parts} method.
1097   * @typedef {Object} FooGallery.utils.url~Parts
1098   * @property {string} hash - A string containing a `#` followed by the fragment identifier of the URL.
1099   * @property {string} host - A string containing the host, that is the hostname, a `:`, and the port of the URL.
1100   * @property {string} hostname - A string containing the domain of the URL.
1101   * @property {string} href - A string containing the entire URL.
1102   * @property {string} origin - A string containing the canonical form of the origin of the specific location.
1103   * @property {string} pathname - A string containing an initial `/` followed by the path of the URL.
1104   * @property {string} port - A string containing the port number of the URL.
1105   * @property {string} protocol - A string containing the protocol scheme of the URL, including the final `:`.
1106   * @property {string} search - A string containing a `?` followed by the parameters of the URL. Also known as "querystring".
1107   * @see {@link FooGallery.utils.url.parts} for example usage.
1108   */
1109
1110})( // dependencies
1111FooGallery.utils, FooGallery.utils.is);
1112
1113(function (_, _is, _fn) {
1114  // only register methods if this version is the current version
1115  if (_.version !== '1.0.1') return;
1116  /**
1117   * @summary Contains common string utility methods.
1118   * @memberof FooGallery.utils.
1119   * @namespace str
1120   */
1121
1122  _.str = {};
1123  /**
1124   * @summary Removes whitespace from both ends of the target string.
1125   * @memberof FooGallery.utils.str.
1126   * @function trim
1127   * @param {string} target - The string to trim.
1128   * @returns {string|null} Returns `null` if the supplied target is not a string.
1129   */
1130
1131  _.str.trim = function (target) {
1132    return _is.string(target) ? target.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '') : null;
1133  };
1134  /**
1135   * @summary Converts the given `target` to camel case.
1136   * @memberof FooGallery.utils.str.
1137   * @function camel
1138   * @param {string} target - The string to camel case.
1139   * @returns {string}
1140   * @example {@run true}
1141   * // alias the FooGallery.utils.str namespace
1142   * var _str = FooGallery.utils.str;
1143   *
1144   * console.log( _str.camel( "max-width" ) ); // => "maxWidth"
1145   * console.log( _str.camel( "max--width" ) ); // => "maxWidth"
1146   * console.log( _str.camel( "max Width" ) ); // => "maxWidth"
1147   * console.log( _str.camel( "Max_width" ) ); // => "maxWidth"
1148   * console.log( _str.camel( "MaxWidth" ) ); // => "maxWidth"
1149   * console.log( _str.camel( "Abbreviations like CSS are left intact" ) ); // => "abbreviationsLikeCSSAreLeftIntact"
1150   */
1151
1152
1153  _.str.camel = function (target) {
1154    if (!_is.string(target)) return target;
1155    if (target.toUpperCase() === target) return target.toLowerCase();
1156    return target.replace(/^([A-Z])|[-\s_]+(\w)/g, function (match, p1, p2) {
1157      if (_is.string(p2)) return p2.toUpperCase();
1158      return p1.toLowerCase();
1159    });
1160  };
1161  /**
1162   * @summary Converts the given `target` to kebab case. Non-alphanumeric characters are converted to `-`.
1163   * @memberof FooGallery.utils.str.
1164   * @function kebab
1165   * @param {string} target - The string to kebab case.
1166   * @returns {string}
1167   * @example {@run true}
1168   * // alias the FooGallery.utils.str namespace
1169   * var _str = FooGallery.utils.str;
1170   *
1171   * console.log( _str.kebab( "max-width" ) ); // => "max-width"
1172   * console.log( _str.kebab( "max--width" ) ); // => "max-width"
1173   * console.log( _str.kebab( "max Width" ) ); // => "max-width"
1174   * console.log( _str.kebab( "Max_width" ) ); // => "max-width"
1175   * console.log( _str.kebab( "MaxWidth" ) ); // => "max-width"
1176   * console.log( _str.kebab( "Non-alphanumeric ch@racters are converted to dashes!" ) ); // => "non-alphanumeric-ch-racters-are-converted-to-dashes"
1177   */
1178
1179
1180  _.str.kebab = function (target) {
1181    if (!_is.string(target)) return target;
1182    return target.match(/[A-Z]{2,}
1182(?=[A-Z][a-z0-9]*|\b)|[A-Z]?[a-z0-9]*|[A-Z]|[0-9]+/g).filter(Boolean).map(function (x) {
1183      return x.toLowerCase();
1184    }).join('-');
1185  };
1186  /**
1187   * @summary Checks if the `target` contains the given `substr`.
1188   * @memberof FooGallery.utils.str.
1189   * @function contains
1190   * @param {string} target - The string to check.
1191   * @param {string} substr - The string to check for.
1192   * @param {boolean} [ignoreCase=false] - Whether or not to ignore casing when performing the check.
1193   * @returns {boolean} `true` if the `target` contains the given `substr`.
1194   * @example {@run true}
1195   * // alias the FooGallery.utils.str namespace
1196   * var _str = FooGallery.utils.str,
1197   * 	// create a string to test
1198   * 	target = "To be, or not to be, that is the question.";
1199   *
1200   * console.log( _str.contains( target, "To be" ) ); // => true
1201   * console.log( _str.contains( target, "question" ) ); // => true
1202   * console.log( _str.contains( target, "no" ) ); // => true
1203   * console.log( _str.contains( target, "nonexistent" ) ); // => false
1204   * console.log( _str.contains( target, "TO BE" ) ); // => false
1205   * console.log( _str.contains( target, "TO BE", true ) ); // => true
1206   */
1207
1208
1209  _.str.contains = function (target, substr, ignoreCase) {
1210    if (!_is.string(target) || _is.empty(target) || !_is.string(substr) || _is.empty(substr)) return false;
1211    return substr.length <= target.length && (!!ignoreCase ? target.toUpperCase().indexOf(substr.toUpperCase()) : target.indexOf(substr)) !== -1;
1212  };
1213  /**
1214   * @summary Checks if the `target` contains the given `word`.
1215   * @memberof FooGallery.utils.str.
1216   * @function containsWord
1217   * @param {string} target - The string to check.
1218   * @param {string} word - The word to check for.
1219   * @param {boolean} [ignoreCase=false] - Whether or not to ignore casing when performing the check.
1220   * @returns {boolean} `true` if the `target` contains the given `word`.
1221   * @description This method differs from {@link FooGallery.utils.str.contains} in that it searches for whole words by splitting the `target` string on word boundaries (`\b`) and then comparing the individual parts.
1222   * @example {@run true}
1223   * // alias the FooGallery.utils.str namespace
1224   * var _str = FooGallery.utils.str,
1225   * 	// create a string to test
1226   * 	target = "To be, or not to be, that is the question.";
1227   *
1228   * console.log( _str.containsWord( target, "question" ) ); // => true
1229   * console.log( _str.containsWord( target, "no" ) ); // => false
1230   * console.log( _str.containsWord( target, "NOT" ) ); // => false
1231   * console.log( _str.containsWord( target, "NOT", true ) ); // => true
1232   * console.log( _str.containsWord( target, "nonexistent" ) ); // => false
1233   */
1234
1235
1236  _.str.containsWord = function (target, word, ignoreCase) {
1237    if (!_is.string(target) || _is.empty(target) || !_is.string(word) || _is.empty(word) || target.length < word.length) return false;
1238    var parts = target.split(/\W/);
1239    var i = 0,
1240        len = parts.length;
1241
1242    for (; i < len; i++) {
1243      if (ignoreCase ? parts[i].toUpperCase() === word.toUpperCase() : parts[i] === word) return true;
1244    }
1245
1246    return false;
1247  };
1248  /**
1249   * @summary Checks if the `target` ends with the given `substr`.
1250   * @memberof FooGallery.utils.str.
1251   * @function endsWith
1252   * @param {string} target - The string to check.
1253   * @param {string} substr - The substr to check for.
1254   * @returns {boolean} `true` if the `target` ends with the given `substr`.
1255   * @example {@run true}
1256   * // alias the FooGallery.utils.str namespace
1257   * var _str = FooGallery.utils.str;
1258   *
1259   * console.log( _str.endsWith( "something", "g" ) ); // => true
1260   * console.log( _str.endsWith( "something", "ing" ) ); // => true
1261   * console.log( _str.endsWith( "something", "no" ) ); // => false
1262   */
1263
1264
1265  _.str.endsWith = function (target, substr) {
1266    if (!_is.string(target) || !_is.string(substr) || substr.length > target.length) return false;
1267    return target.slice(target.length - substr.length) === substr;
1268  };
1269  /**
1270   * @summary Checks if the `target` starts with the given `substr`.
1271   * @memberof FooGallery.utils.str.
1272   * @function startsWith
1273   * @param {string} target - The string to check.
1274   * @param {string} substr - The substr to check for.
1275   * @returns {boolean} `true` if the `target` starts with the given `substr`.
1276   * @example {@run true}
1277   * // alias the FooGallery.utils.str namespace
1278   * var _str = FooGallery.utils.str;
1279   *
1280   * console.log( _str.startsWith( "something", "s" ) ); // => true
1281   * console.log( _str.startsWith( "something", "some" ) ); // => true
1282   * console.log( _str.startsWith( "something", "no" ) ); // => false
1283   */
1284
1285
1286  _.str.startsWith = function (target, substr) {
1287    if (_is.empty(target) || _is.empty(substr)) return false;
1288    return target.slice(0, substr.length) === substr;
1289  };
1290  /**
1291   * @summary Escapes the `target` for use in a regular expression.
1292   * @memberof FooGallery.utils.str.
1293   * @function escapeRegExp
1294   * @param {string} target - The string to escape.
1295   * @returns {string}
1296   * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
1297   */
1298
1299
1300  _.str.escapeRegExp = function (target) {
1301    if (!_is.string(target)) return target;
1302    return target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1303  };
1304  /**
1305   * @summary Generates a 32 bit FNV-1a hash from the given `target`.
1306   * @memberof FooGallery.utils.str.
1307   * @function fnv1a
1308   * @param {string} target - The string to generate a hash from.
1309   * @returns {?number} `null` if the `target` is not a string or empty otherwise a 32 bit FNV-1a hash.
1310   * @example {@run true}
1311   * // alias the FooGallery.utils.str namespace
1312   * var _str = FooGallery.utils.str;
1313   *
1314   * console.log( _str.fnv1a( "Some string to generate a hash for." ) ); // => 207568994
1315   * console.log( _str.fnv1a( "Some string to generate a hash for" ) ); // => 1350435704
1316   * @see {@link https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function|Fowler–Noll–Vo hash function}
1317   */
1318
1319
1320  _.str.fnv1a = function (target) {
1321    if (!_is.string(target) || _is.empty(target)) return null;
1322    var i,
1323        l,
1324        hval = 0x811c9dc5;
1325
1326    for (i = 0, l = target.length; i < l; i++) {
1327      hval ^= target.charCodeAt(i);
1328      hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
1329    }
1330
1331    return hval >>> 0;
1332  };
1333  /**
1334   * @summary Returns the remainder of the `target` split on the first index of the given `substr`.
1335   * @memberof FooGallery.utils.str.
1336   * @function from
1337   * @param {string} target - The string to split.
1338   * @param {string} substr - The substring to split on.
1339   * @returns {?string} `null` if the given `substr` does not exist within the `target`.
1340   * @example {@run true}
1341   * // alias the FooGallery.utils.str namespace
1342   * var _str = FooGallery.utils.str,
1343   * 	// create a string to test
1344   * 	target = "To be, or not to be, that is the question.";
1345   *
1346   * console.log( _str.from( target, "no" ) ); // => "t to be, that is the question."
1347   * console.log( _str.from( target, "that" ) ); // => " is the question."
1348   * console.log( _str.from( target, "question" ) ); // => "."
1349   * console.log( _str.from( target, "nonexistent" ) ); // => null
1350   */
1351
1352
1353  _.str.from = function (target, substr) {
1354    return _.str.contains(target, substr) ? target.substring(target.indexOf(substr) + substr.length) : null;
1355  };
1356  /**
1357   * @summary Joins any number of strings using the given `separator`.
1358   * @memberof FooGallery.utils.str.
1359   * @function join
1360   * @param {string} separator - The separator to use to join the strings.
1361   * @param {string} part - The first string to join.
1362   * @param {...string} [partN] - Any number of additional strings to join.
1363   * @returns {?string}
1364   * @description This method differs from using the standard `Array.prototype.join` function to join strings in that it ignores empty parts and checks to see if each starts with the supplied `separator`. If the part starts with the `separator` it is removed before appending it to the final result.
1365   * @example {@run true}
1366   * // alias the FooGallery.utils.str namespace
1367   * var _str = FooGallery.utils.str;
1368   *
1369   * console.log( _str.join( "_", "all", "in", "one" ) ); // => "all_in_one"
1370   * console.log( _str.join( "_", "all", "_in", "one" ) ); // => "all_in_one"
1371   * console.log( _str.join( "/", "http://", "/example.com/", "/path/to/image.png" ) ); // => "http://example.com/path/to/image.png"
1372   * console.log( _str.join( "/", "http://", "/example.com", "/path/to/image.png" ) ); // => "http://example.com/path/to/image.png"
1373   * console.log( _str.join( "/", "http://", "example.com", "path/to/image.png" ) ); // => "http://example.com/path/to/image.png"
1374   */
1375
1376
1377  _.str.join = function (separator, part, partN) {
1378    if (!_is.string(separator) || !_is.string(part)) return null;
1379
1380    var parts = _fn.arg2arr(arguments);
1381
1382    separator = parts.shift();
1383    var i,
1384        l,
1385        result = parts.shift();
1386
1387    for (i = 0, l = parts.length; i < l; i++) {
1388      part = parts[i];
1389      if (_is.empty(part)) continue;
1390
1391      if (_.str.endsWith(result, separator)) {
1392        result = result.slice(0, result.length - separator.length);
1393      }
1394
1395      if (_.str.startsWith(part, separator)) {
1396        part = part.slice(separator.length);
1397      }
1398
1399      result += separator + part;
1400    }
1401
1402    return result;
1403  };
1404  /**
1405   * @summary Returns the first part of the `target` split on the first index of the given `substr`.
1406   * @memberof FooGallery.utils.str.
1407   * @function until
1408   * @param {string} target - The string to split.
1409   * @param {string} substr - The substring to split on.
1410   * @returns {string} The `target` if the `substr` does not exist.
1411   * @example {@run true}
1412   * // alias the FooGallery.utils.str namespace
1413   * var _str = FooGallery.utils.str,
1414   * 	// create a string to test
1415   * 	target = "To be, or not to be, that is the question.";
1416   *
1417   * console.log( _str.until( target, "no" ) ); // => "To be, or "
1418   * console.log( _str.until( target, "that" ) ); // => "To be, or not to be, "
1419   * console.log( _str.until( target, "question" ) ); // => "To be, or not to be, that is the "
1420   * console.log( _str.until( target, "nonexistent" ) ); // => "To be, or not to be, that is the question."
1421   */
1422
1423
1424  _.str.until = function (target, substr) {
1425    return _.str.contains(target, substr) ? target.substring(0, target.indexOf(substr)) : target;
1426  };
1427  /**
1428   * @summary A basic string formatter that can use both index and name based placeholders but handles only string or number replacements.
1429   * @memberof FooGallery.utils.str.
1430   * @function format
1431   * @param {string} target - The format string containing any placeholders to replace.
1432   * @param {string|number|Object|Array} arg1 - The first value to format the target with. If an object is supplied it's properties are used to match named placeholders. If an array, string or number is supplied it's values are used to match any index placeholders.
1433   * @param {...(string|number)} [argN] - Any number of additional strings or numbers to format the target with.
1434   * @returns {string} The string formatted with the supplied arguments.
1435   * @description This method allows you to supply the replacements as an object when using named placeholders or as an array or additional arguments when using index placeholders.
1436   *
1437   * This does not perform a simultaneous replacement of placeholders, which is why it's referred to as a basic formatter. This means replacements that contain placeholders within there value could end up being replaced themselves as seen in the last example.
1438   * @example {@caption The following shows how to use index placeholders.}{@run true}
1439   * // alias the FooGallery.utils.str namespace
1440   * var _str = FooGallery.utils.str,
1441   * 	// create a format string using index placeholders
1442   * 	format = "Hello, {0}, are you feeling {1}?";
1443   *
1444   * console.log( _str.format( format, "Steve", "OK" ) ); // => "Hello, Steve, are you feeling OK?"
1445   * // or
1446   * console.log( _str.format( format, [ "Steve", "OK" ] ) ); // => "Hello, Steve, are you feeling OK?"
1447   * @example {@caption While the above works perfectly fine the downside is that the placeholders provide no clues as to what should be supplied as a replacement value, this is were supplying an object and using named placeholders steps in.}{@run true}
1448   * // alias the FooGallery.utils.str namespace
1449   * var _str = FooGallery.utils.str,
1450   * 	// create a format string using named placeholders
1451   * 	format = "Hello, {name}, are you feeling {adjective}?";
1452   *
1453   * console.log( _str.format( format, {name: "Steve", adjective: "OK"} ) ); // => "Hello, Steve, are you feeling OK?"
1454   * @example {@caption The following demonstrates the issue with not performing a simultaneous replacement of placeholders.}{@run true}
1455   * // alias the FooGallery.utils.str namespace
1456   * var _str = FooGallery.utils.str;
1457   *
1458   * console.log( _str.format("{0}{1}", "{1}", "{0}") ); // => "{0}{0}"
1459   *
1460   * // If the replacement happened simultaneously the result would be "{1}{0}" but this method executes
1461   * // replacements synchronously as seen below:
1462   *
1463   * // "{0}{1}".replace( "{0}", "{1}" )
1464   * // => "{1}{1}".replace( "{1}", "{0}" )
1465   * // => "{0}{0}"
1466   */
1467
1468
1469  _.str.format = function (target, arg1, argN) {
1470    var args = _fn.arg2arr(arguments);
1471
1472    target = args.shift(); // remove the target from the args
1473
1474    if (_is.string(target) && args.length > 0) {
1475      if (args.length === 1 && (_is.array(args[0]) || _is.object(args[0]))) {
1476        args = args[0];
1477      }
1478
1479      _.each(args, function (value, placeholder) {
1480        target = target.replace(new RegExp("\\{" + placeholder + "\\}", "gi"), value + "");
1481      });
1482    }
1483
1484    return target;
1485  };
1486})( // dependencies
1487FooGallery.utils, FooGallery.utils.is, FooGallery.utils.fn);
1488
1489(function ($, _, _is, _fn, _str) {
1490  // only register methods if this version is the current version
1491  if (_.version !== '1.0.1') return;
1492  /**
1493   * @summary Contains common object utility methods.
1494   * @memberof FooGallery.utils.
1495   * @namespace obj
1496   */
1497
1498  _.obj = {};
1499  /**
1500   * @summary Creates a new object with the specified prototype.
1501   * @memberof FooGallery.utils.obj.
1502   * @function create
1503   * @param {Object} proto - The object which should be the prototype of the newly-created object.
1504   * @returns {Object} A new object with the specified prototype.
1505   * @description This is a basic implementation of the {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create|Object.create} method.
1506   */
1507
1508  _.obj.create = function (proto) {
1509    if (!_is.object(proto)) throw TypeError('Argument must be an object');
1510
1511    function Obj() {}
1512
1513    Obj.prototype = proto;
1514    return new Obj();
1515  };
1516  /**
1517   * @summary Merge the contents of two or more objects together into the first `target` object.
1518   * @memberof FooGallery.utils.obj.
1519   * @function extend
1520   * @param {Object} target - The object to merge properties into.
1521   * @param {Object}
1521 object - An object containing properties to merge.
1522   * @param {...Object} [objectN] - Additional objects containing properties to merge.
1523   * @returns {Object} The `target` merged with the contents from any additional objects.
1524   * @description This does not merge arrays by index as jQuery does, it treats them as a single property and replaces the array with a shallow copy of the new one.
1525   *
1526   * This method makes use of the {@link FooGallery.utils.obj.merge} method internally.
1527   * @example {@run true}
1528   * // alias the FooGallery.utils.obj namespace
1529   * var _obj = FooGallery.utils.obj,
1530   * 	// create some objects to merge
1531   * 	defaults = {"name": "My Object", "enabled": false, "arr": [1,2,3]},
1532   * 	options = {"enabled": true, "something": 123, "arr": [4,5,6]};
1533   *
1534   * // merge the two objects into a new third one without modifying either of the originals
1535   * var settings = _obj.extend( {}, defaults, options );
1536   *
1537   * console.log( settings ); // => {"name": "My Object", "enabled": true, "arr": [4,5,6], "something": 123}
1538   * console.log( defaults ); // => {"name": "My Object", "enabled": true, "arr": [1,2,3]}
1539   * console.log( options ); // => {"enabled": true, "arr": [4,5,6], "something": 123}
1540   */
1541
1542
1543  _.obj.extend = function (target, object, objectN) {
1544    target = _is.object(target) ? target : {};
1545
1546    var objects = _fn.arg2arr(arguments);
1547
1548    objects.shift();
1549
1550    _.each(objects, function (object) {
1551      _.obj.merge(target, object);
1552    });
1553
1554    return target;
1555  };
1556  /**
1557   * @summary Merge the contents of two objects together into the first `target` object.
1558   * @memberof FooGallery.utils.obj.
1559   * @function merge
1560   * @param {Object} target - The object to merge properties into.
1561   * @param {Object} object - The object containing properties to merge.
1562   * @returns {Object} The `target` merged with the contents from the `object`.
1563   * @description This does not merge arrays by index as jQuery does, it treats them as a single property and replaces the array with a shallow copy of the new one.
1564   *
1565   * This method is used internally by the {@link FooGallery.utils.obj.extend} method.
1566   * @example {@run true}
1567   * // alias the FooGallery.utils.obj namespace
1568   * var _obj = FooGallery.utils.obj,
1569   * 	// create some objects to merge
1570   * 	target = {"name": "My Object", "enabled": false, "arr": [1,2,3]},
1571   * 	object = {"enabled": true, "something": 123, "arr": [4,5,6]};
1572   *
1573   * console.log( _obj.merge( target, object ) ); // => {"name": "My Object", "enabled": true, "arr": [4,5,6], "something": 123}
1574   */
1575
1576
1577  _.obj.merge = function (target, object) {
1578    target = _is.hash(target) ? target : {};
1579    object = _is.hash(object) ? object : {};
1580
1581    for (var prop in object) {
1582      if (object.hasOwnProperty(prop)) {
1583        if (_is.hash(object[prop])) {
1584          target[prop] = _is.hash(target[prop]) ? target[prop] : {};
1585
1586          _.obj.merge(target[prop], object[prop]);
1587        } else if (_is.array(object[prop])) {
1588          target[prop] = object[prop].slice();
1589        } else {
1590          target[prop] = object[prop];
1591        }
1592      }
1593    }
1594
1595    return target;
1596  };
1597  /**
1598   * @summary Merge the validated properties of the `object` into the `target` using the optional `mappings`.
1599   * @memberof FooGallery.utils.obj.
1600   * @function mergeValid
1601   * @param {Object} target - The object to merge properties into.
1602   * @param {FooGallery.utils.obj~Validators} validators - An object containing validators for the `target` object properties.
1603   * @param {Object} object - The object containing properties to merge.
1604   * @param {FooGallery.utils.obj~Mappings} [mappings] - An object containing property name mappings.
1605   * @returns {Object} The modified `target` object containing any valid properties from the supplied `object`.
1606   * @example {@caption Shows the basic usage for this method and shows how invalid properties or those with no corresponding validator are ignored.}{@run true}
1607   * // alias the FooGallery.utils.obj and FooGallery.utils.is namespaces
1608   * var _obj = FooGallery.utils.obj,
1609   * 	_is = FooGallery.utils.is;
1610   *
1611   * //create the target object and it's validators
1612   * var target = {"name":"John","location":"unknown"},
1613   * 	validators = {"name":_is.string,"location":_is.string};
1614   *
1615   * // create the object to merge into the target
1616   * var object = {
1617   * 	"name": 1234, // invalid
1618   * 	"location": "Liverpool", // updated
1619   * 	"notMerged": true // ignored
1620   * };
1621   *
1622   * // merge the object into the target, invalid properties or those with no corresponding validator are ignored.
1623   * console.log( _obj.mergeValid( target, validators, object ) ); // => { "name": "John", "location": "Liverpool" }
1624   * @example {@caption Shows how to supply a mappings object for this method.}{@run true}
1625   * // alias the FooGallery.utils.obj and FooGallery.utils.is namespaces
1626   * var _obj = FooGallery.utils.obj,
1627   * 	_is = FooGallery.utils.is;
1628   *
1629   * //create the target object and it's validators
1630   * var target = {"name":"John","location":"unknown"},
1631   * 	validators = {"name":_is.string,"location":_is.string};
1632   *
1633   * // create the object to merge into the target
1634   * var object = {
1635   * 	"name": { // ignored
1636   * 		"proper": "Christopher", // mapped to name if short is invali
1636d
1637   * 		"short": "Chris" // map to name
1638   * 	},
1639   * 	"city": "London" // map to location
1640   * };
1641   *
1642   * // create the mapping object
1643   * var mappings = {
1644   * 	"name": [ "name.short", "name.proper" ], // try use the short name and fallback to the proper
1645   * 	"location": "city"
1646   * };
1647   *
1648   * // merge the object into the target using the mappings, invalid properties or those with no corresponding validator are ignored.
1649   * console.log( _obj.mergeValid( target, validators, object, mappings ) ); // => { "name": "Chris", "location": "London" }
1650   */
1651
1652
1653  _.obj.mergeValid = function (target, validators, object, mappings) {
1654    if (!_is.hash(object) || !_is.hash(validators)) return target;
1655    validators = _is.hash(validators) ? validators : {};
1656    mappings = _is.hash(mappings) ? mappings : {};
1657    var prop, maps, value;
1658
1659    for (prop in validators) {
1660      if (!validators.hasOwnProperty(prop) || !_is.fn(validators[prop])) continue;
1661      maps = _is.array(mappings[prop]) ? mappings[prop] : _is.string(mappings[prop]) ? [mappings[prop]] : [prop];
1662
1663      _.each(maps, function (map) {
1664        value = _.obj.prop(object, map);
1665        if (_is.undef(value)) return; // continue
1666
1667        if (validators[prop](value)) {
1668          _.obj.prop(target, prop, value);
1669
1670          return false; // break
1671        }
1672      });
1673    }
1674
1675    return target;
1676  };
1677  /**
1678   * @summary Get or set a property value given its `name`.
1679   * @memberof FooGallery.utils.obj.
1680   * @function prop
1681   * @param {Object} object - The object to inspect for the property.
1682   * @param {string} name - The name of the property to fetch. This can be a `.` notated name.
1683   * @param {*} [value] - If supplied this is the value to set for the property.
1684   * @returns {*} The value for the `name` property, if it does not exist then `undefined`.
1685   * @returns {undefined} If a `value` is supplied this method returns nothing.
1686   * @example {@caption Shows how to get a property value from an object.}{@run true}
1687   * // alias the FooGallery.utils.obj namespace
1688   * var _obj = FooGallery.utils.obj,
1689   * 	// create an object to test
1690   * 	object = {
1691   * 		"name": "My Object",
1692   * 		"some": {
1693   * 			"thing": 123
1694   * 		}
1695   * 	};
1696   *
1697   * console.log( _obj.prop( object, "name" ) ); // => "My Object"
1698   * console.log( _obj.prop( object, "some.thing" ) ); // => 123
1699   * @example {@caption Shows how to set a property value for an object.}{@run true}
1700   * // alias the FooGallery.utils.obj namespace
1701   * var _obj = FooGallery.utils.obj,
1702   * 	// create an object to test
1703   * 	object = {
1704   * 		"name": "My Object",
1705   * 		"some": {
1706   * 			"thing": 123
1707   * 		}
1708   * 	};
1709   *
1710   * _obj.prop( object, "name", "My Updated Object" );
1711   * _obj.prop( object, "some.thing", 987 );
1712   *
1713   * console.log( object ); // => { "name": "My Updated Object", "some": { "thing": 987 } }
1714   */
1715
1716
1717  _.obj.prop = function (object, name, value) {
1718    if (!_is.object(object) || _is.empty(name)) return;
1719    var parts, last;
1720
1721    if (_is.undef(value)) {
1722      if (_str.contains(name, '.')) {
1723        parts = name.split('.');
1724        last = parts.length - 1;
1725
1726        _.each(parts, function (part, i) {
1727          if (i === last) {
1728            value = object[part];
1729          } else if (_is.hash(object[part])) {
1730            object = object[part];
1731          } else {
1732            // exit early
1733            return false;
1734          }
1735        });
1736      } else if (!_is.undef(object[name])) {
1737        value = object[name];
1738      }
1739
1740      return value;
1741    }
1742
1743    if (_str.contains(name, '.')) {
1744      parts = name.split('.');
1745      last = parts.length - 1;
1746
1747      _.each(parts, function (part, i) {
1748        if (i === last) {
1749          object[part] = value;
1750        } else {
1751          object = _is.hash(object[part]) ? object[part] : object[part] = {};
1752        }
1753      });
1754    } else if (!_is.undef(object[name])) {
1755      object[name] = value;
1756    }
1757  }; //######################
1758  //## Type Definitions ##
1759  //######################
1760
1761  /**
1762   * @summary An object used by the {@link FooGallery.utils.obj.mergeValid|mergeValid} method to map new values onto the `target` object.
1763   * @typedef {Object.<string,(string|Array.<string>)>} FooGallery.utils.obj~Mappings
1764   * @description The mappings object is a single level object. If you want to map a property from/to a child object on either the source or target objects you must supply the name using `.` notation as seen in the below example with the `"name.first"` to `"Name.Short"` mapping.
1765   * @example {@caption The basic structure of a mappings object is the below.}
1766   * {
1767   * 	"TargetName": "SourceName", // for top level properties
1768   * 	"Child.TargetName": "Child.SourceName" // for child properties
1769   * }
1770   * @example {@caption Given the following target object.}
1771   * var target = {
1772   * 	"name": {
1773   * 		"first": "",
1774   * 		"last": null
1775   * 	},
1776   * 	"age": 0
1777   * };
1778   * @example {@caption And the following object to merge.}
1779   * var object = {
1780   * 	"Name": {
1781   * 		"Full": "Christopher",
1782   * 		"Short": "Chris"
1783   * 	},
1784   * 	"Age": 32
1785   * };
1786   * @example {@caption The mappings object would look like the below.}
1787   * var mappings = {
1788   * 	"name.first": "Name.Short",
1789   * 	"age": "Age"
1790   * };
1791   * @example {@caption If you want the `"name.first"` property to try to use the `"Name.Short"` value but fallback to `"Name.Proper"` you can specify the mapping value as an array.}
1792   * var mappings = {
1793   * 	"name.first": [ "Name.Short", "Name.Proper" ],
1794   * 	"age": "Age"
1795   * };
1796   */
1797
1798  /**
1799   * @summary An object used by the {@link FooGallery.utils.obj.mergeValid|mergeValid} method to validate properties.
1800   * @typedef {Object.<string,function(*):boolean>} FooGallery.utils.obj~Validators
1801   * @description The validators object is a single level object. If you want to validate a property of a child object you must supply the name using `.` notation as seen in the below example with the `"name.first"` and `"name.last"` properties.
1802   *
1803   * Any function that accepts a value to test as the first argument and returns a boolean can be used as a validator. This means the majority of the {@link FooGallery.utils.is} methods can be used directly. If the property supports multiple types just provide your own function as seen with `"name.last"` in the below example.
1804   * @example {@caption The basic structure of a validators object is the below.}
1805   * {
1806   * 	"PropName": function(*):boolean, // for top level properties
1807   * 	"Child.PropName": function(*):boolean // for child properties
1808   * }
1809   * @example {@caption Given the following target object.}
1810   * var target = {
1811   * 	"name": {
1812   * 		"first": "", // must be a string
1813   * 		"last": null // must be a string or null
1814   * 	},
1815   * 	"age": 0 // must be a number
1816   * };
1817   * @example {@caption The validators object could be created as seen below.}
1818   * // alias the FooGallery.utils.is namespace
1819   * var _is = FooGallery.utils.is;
1820   *
1821   * var validators = {
1822   * 	"name.first": _is.string,
1823   * 	"name.last": function(value){
1824   * 		return _is.string(value) || value === null;
1825   * 	},
1826   * 	"age": _is.number
1827   * };
1828   */
1829
1830})( // dependencies
1831FooGallery.utils.$, FooGallery.utils, FooGallery.utils.is, FooGallery.utils.fn, FooGallery.utils.str);
1832
1833(function ($, _, _is) {
1834  // only register methods if this version is the current version
1835  if (_.version !== '1.0.1') return; // any methods that have dependencies but don't fall into a specific subset or namespace can be added here
1836
1837  /**
1838   * @summary The callback for the {@link FooGallery.utils.ready} method.
1839   * @callback FooGallery.utils~readyCallback
1840   * @param {jQuery} $ - The instance of jQuery the plugin was registered with.
1841   * @this window
1842   * @see Take a look at the {@link FooGallery.utils.ready} method for example usage.
1843   */
1844
1845  /**
1846   * @summary Waits for the DOM to be accessible and then executes the supplied callback.
1847   * @memberof FooGallery.utils.
1848   * @function ready
1849   * @param {FooGallery.utils~readyCallback} callback - The function to execute once the DOM is accessible.
1850   * @example {@caption This method can be used as a replacement for the jQuery ready callback to avoid an error in another script stopping our scripts from running.}
1851   * FooGallery.utils.ready(function($){
1852   * 	// do something
1853   * });
1854   */
1855
1856  _.ready = function (callback) {
1857    function onready() {
1858      try {
1859        callback.call(window, _.$);
1860      } catch (err) {
1861        console.error(err);
1862      }
1863    }
1864
1865    if (document.readyState !== "loading") onready();else document.addEventListener('DOMContentLoaded', onready, false);
1866  };
1867  /**
1868   * @summary Executed once for each array index or object property until it returns a truthy value.
1869   * @callback FooGallery.utils~findCallback
1870   * @param {*} value - The current value being iterated over. This could be either an element in an array or the value of an object property.
1871   * @param {(number|string)} [key] - The array index or property name of the `value`.
1872   * @param {(Object|Array)} [object] - The array or object currently being searched.
1873   * @returns {boolean} A truthy value.
1874   */
1875
1876  /**
1877   * @summary Returns the value of the first element or property in the provided target that satisfies the provided test function.
1878   * @memberof FooGallery.utils.
1879   * @function find
1880   * @param {(Object|Array)} target - The object or array to search.
1881   * @param {FooGallery.utils~findCallback} callback - A function to execute for each value in the target.
1882   * @param {*} [thisArg] - The `this` value within the `callback`.
1883   * @returns {*} The value of the first element or property in the provided target that satisfies the provided test function. Otherwise, `undefined` is returned.
1884   */
1885
1886
1887  _.find = function (target, callback, thisArg) {
1888    if (!_is.fn(callback)) return;
1889    thisArg = _is.undef(thisArg) ? callback : thisArg;
1890    var i, l;
1891
1892    if (_is.array(target)) {
1893      for (i = 0, l = target.length; i < l; i++) {
1894        if (callback.call(thisArg, target[i], i, target)) {
1895          return target[i];
1896        }
1897      }
1898    } else if (_is.object(target)) {
1899      var keys = Object.keys(target);
1900
1901      for (i = 0, l = keys.length; i < l; i++) {
1902        if (callback.call(thisArg, target[keys[i]], keys[i], target)) {
1903          return target[keys[i]];
1904        }
1905      }
1906    }
1907  };
1908  /**
1909   * @summary Executed once for each array index or object property.
1910   * @callback FooGallery.utils~eachCallback
1911   * @param {*} value - The current value being iterated over. This could be either an element in an array or the value of an object property.
1912   * @param {(number|string)} [key] - The array index or property name of the `value`.
1913   * @param {(Object|Array)} [object] - The array or object currently being searched.
1914   * @returns {(boolean|void)} Return `false` to break out of the loop, all other values are ignored.
1915   */
1916
1917  /**
1918   * @summary Iterate over all indexes or properties of the provided target executing the provided callback once per value.
1919   * @memberof FooGallery.utils.
1920   * @function each
1921   * @param {(Object|Array)} object - The object or array to search.
1922   * @param {FooGallery.utils~eachCallback} callback - A function to execute for each value in the target.
1923   * @param {*} [thisArg] - The `this` value within the `callback`.
1924   */
1925
1926
1927  _.each = function (object, callback, thisArg) {
1928    if (!_is.fn(callback)) return;
1929    thisArg = _is.undef(thisArg) ? callback : thisArg;
1930    var i, l, result;
1931
1932    if (_is.array(object)) {
1933      for (i = 0, l = object.length; i < l; i++) {
1934        result = callback.call(thisArg, object[i], i, object);
1935        if (result === false) break;
1936      }
1937    } else if (_is.object(object)) {
1938      var keys = Object.keys(object);
1939
1940      for (i = 0, l = keys.length; i < l; i++) {
1941        result = callback.call(thisArg, object[keys[i]], keys[i], object);
1942        if (result === false) break;
1943      }
1944    }
1945  };
1946  /**
1947   * @summary Checks if a value exists within an array.
1948   * @memberof FooGallery.utils.
1949   * @function inArray
1950   * @param {*} needle - The value to search for.
1951   * @param {Array} haystack - The array to search within.
1952   * @returns {number} Returns the index of the value if found otherwise -1.
1953   */
1954
1955
1956  _.inArray = function (needle, haystack) {
1957    if (_is.array(haystack)) {
1958      return haystack.indexOf(needle);
1959    }
1960
1961    return -1;
1962  };
1963  /**
1964   * @summary Convert CSS class names into CSS selectors.
1965   * @memberof FooGallery.utils.
1966   * @function selectify
1967   * @param {(string|string[]|object)} classes - A space delimited string of CSS class names or an array of them with each item being included in the selector using the OR (`,`) syntax as a separator. If an object is supplied the result will be an object with the same property names but the values converted to selectors.
1968   * @returns {(object|string)}
1969   * @example {@caption Shows how the method can be used.}
1970   * // alias the FooGallery.utils namespace
1971   * var _ = FooGallery.utils;
1972   *
1973   * console.log( _.selectify("my-class") ); // => ".my-class"
1974   * console.log( _.selectify("my-class my-other-class") ); // => ".my-class.my-other-class"
1975   * console.log( _.selectify(["my-class", "my-other-class"]) ); // => ".my-class,.my-other-class"
1976   * console.log( _.selectify({
1977   * 	class1: "my-class",
1978   * 	class2: "my-class my-other-class",
1979   * 	class3: ["my-class", "my-other-class"]
1980   * }) ); // => { class1: ".my-class", class2: ".my-class.my-other-class", class3: ".my-class,.my-other-class" }
1981   */
1982
1983
1984  _.selectify = function (classes) {
1985    if (_is.empty(classes)) return null;
1986
1987    if (_is.hash(classes)) {
1988      var result = {},
1989          selector;
1990
1991      for (var name in classes) {
1992        if (!classes.hasOwnProperty(name)) continue;
1993        selector = _.selectify(classes[name]);
1994
1995        if (selector) {
1996          result[name] = selector;
1997        }
1998      }
1999
2000      return result;
2001    }
2002
2003    if (_is.string(classes) || _is.array(classes)) {
2004      if (_is.string(classes)) classes = [classes];
2005      return classes.map(function (str) {
2006        return _is.string(str) ? "." + str.split(/\s/g).join(".") : null;
2007      }).join(",");
2008    }
2009
2010    return null;
2011  };
2012  /**
2013   * @ignore
2014   * @summary Internal replacement for the `requestAnimationFrame` method if the browser doesn't support any form of the method.
2015   * @param {function} callback - The function to call when it's time to update your animation for the next repaint.
2016   * @return {number} - The request id that uniquely identifies the entry in the callback list.
2017   */
2018
2019
2020  function raf(callback) {
2021    return setTimeout(callback, 1000 / 60);
2022  }
2023  /**
2024   * @ignore
2025   * @summary Internal replacement for the `cancelAnimationFrame` method if the browser doesn't support any form of the method.
2026   * @param {number} requestID - The ID value returned by the call to {@link FooGallery.utils.requestFrame|requestFrame} that requested the callback.
2027   */
2028
2029
2030  function caf(requestID) {
2031    clearTimeout(requestID);
2032  }
2033  /**
2034   * @summary A cross browser wrapper for the `requestAnimationFrame` method.
2035   * @memberof FooGallery.utils.
2036   * @function requestFrame
2037   * @param {function} callback - The function to call when it's time to update your animation for the next repaint.
2038   * @return {number} - The request id that uniquely identifies the entry in the callback list.
2039   */
2040
2041
2042  _.requestFrame = (window.requestAnimationFrame || window.webkitRequestAnimationFrame || raf).bind(window);
2043  /**
2044   * @summary A cross browser wrapper for the `cancelAnimationFrame` method.
2045   * @memberof FooGallery.utils.
2046   * @function cancelFrame
2047   * @param {number} requestID - The ID value returned by the call to {@link FooGallery.utils.requestFrame|requestFrame} that requested the callback.
2048   */
2049
2050  _.cancelFrame = (window.cancelAnimationFrame || window.webkitCancelAnimationFrame || caf).bind(window);
2051  /**
2052   * @summary Registers a callback with the next available animation frame.
2053   * @memberof FooGallery.utils.
2054   * @function nextFrame
2055   * @param {function} callback - The callback to execute for the next frame.
2056   * @param {*} [thisArg] - The value of `this` within the callback. Defaults to the callback itself.
2057   * @returns {Promise} Returns a promise object that is resolved using the return value of the callback.
2058   */
2059
2060  _.nextFrame = function (callback, thisArg) {
2061    return $.Deferred(function (def) {
2062      if (!_is.fn(callback)) {
2063        def.reject(new Error('Provided callback is not a function.'));
2064      } else {
2065        thisArg = _is.undef(thisArg) ? callback : thisArg;
2066
2067        _.requestFrame(function () {
2068          try {
2069            def.resolve(callback.call(thisArg));
2070          } catch (err) {
2071            def.reject(err);
2072          }
2073        });
2074      }
2075    }).promise();
2076  };
2077})( // dependencies
2078FooGallery.utils.$, FooGallery.utils, FooGallery.utils.is);
2079
2080(function ($, _, _is, _obj, _fn) {
2081  // only register methods if this version is the current version
2082  if (_.version !== '1.0.1') return;
2083  /**
2084   * @summary A base class providing some helper methods for prototypal inheritance.
2085   * @memberof FooGallery.utils.
2086   * @constructs Class
2087   * @description This is a base class for making prototypal inheritance simpler to work with. It provides an easy way to inherit from another class and exposes a `_super` method within the scope of any overriding methods that allows a 
2087simple way to execute the overridden function.
2088   *
2089   * Have a look at the {@link FooGallery.utils.Class.extend|extend} and {@link FooGallery.utils.Class.override|override} method examples to see some basic usage.
2090   * @example {@caption When using this base class the actual construction of a class is performed by the `construct` method.}
2091   * var MyClass = FooGallery.utils.Class.extend({
2092   * 	construct: function(arg1, arg2){
2093   * 		// handle the construction logic here
2094   * 	}
2095   * });
2096   *
2097   * // use the class
2098   * var myClass = new MyClass( "arg1:value", "arg2:value" );
2099   */
2100
2101  _.Class = function () {};
2102  /**
2103   * @ignore
2104   * @summary The original function when within the scope of an overriding method.
2105   * @memberof FooGallery.utils.Class#
2106   * @function _super
2107   * @param {...*} [argN] - The same arguments as the base method.
2108   * @returns {*} The result of the base method.
2109   * @description This is only available within the scope of an overriding method if it was created using the {@link FooGallery.utils.Class.extend|extend}, {@link FooGallery.utils.Class.override|override} or {@link FooGallery.utils.fn.addOrOverride} methods.
2110   * @see {@link FooGallery.utils.fn.addOrOverride} to see an example of how this property is used.
2111   */
2112
2113  /**
2114   * @summary Creates a new class that inherits from this one which in turn allows itself to be extended.
2115   * @memberof FooGallery.utils.Class.
2116   * @function extend
2117   * @param {Object} [definition] - An object containing any methods to implement/override.
2118   * @returns {function} A new class that inherits from the base class.
2119   * @description Every class created using this method has both the {@link FooGallery.utils.Class.extend|extend} and {@link FooGallery.utils.Class.override|override} static methods added to it to allow it to be extended.
2120   * @example {@caption The below shows an example of how to implement inheritance using this method.}{@run true}
2121   * // create a base Person class
2122   * var Person = FooGallery.utils.Class.extend({
2123   * 	construct: function(isDancing){
2124   * 		this.dancing = isDancing;
2125   * 	},
2126   * 	dance: function(){
2127   * 		return this.dancing;
2128   * 	}
2129   * });
2130   *
2131   * var Ninja = Person.extend({
2132   * 	construct: function(){
2133   * 		// Call the inherited version of construct()
2134   * 		this._super( false );
2135   * 	},
2136   * 	dance: function(){
2137   * 		// Call the inherited version of dance()
2138   * 		return this._super();
2139   * 	},
2140   * 	swingSword: function(){
2141   * 		return true;
2142   * 	}
2143   * });
2144   *
2145   * var p = new Person(true);
2146   * console.log( p.dance() ); // => true
2147   *
2148   * var n = new Ninja();
2149   * console.log( n.dance() ); // => false
2150   * console.log( n.swingSword() ); // => true
2151   * console.log(
2152   * 	p instanceof Person && p.constructor === Person && p instanceof FooGallery.utils.Class
2153   * 	&& n instanceof Ninja && n.constructor === Ninja && n instanceof Person && n instanceof FooGallery.utils.Class
2154   * ); // => true
2155   */
2156
2157
2158  _.Class.extend = function (definition) {
2159    definition = _is.hash(definition) ? definition : {};
2160
2161    var proto = _obj.create(this.prototype); // create a new prototype to work with so we don't modify the original
2162    // iterate over all properties in the supplied definition and update the prototype
2163
2164
2165    for (var name in definition) {
2166      if (!definition.hasOwnProperty(name)) continue;
2167
2168      _fn.addOrOverride(proto, name, definition[name]);
2169    } // if no construct method is defined add a default one that does nothing
2170
2171
2172    proto.construct = _is.fn(proto.construct) ? proto.construct : function () {}; // create the new class using the prototype made above
2173
2174    function Class() {
2175      if (!_is.fn(this.construct)) throw new SyntaxError('FooGallery.utils.Class objects must be constructed with the "new" keyword.');
2176      this.construct.apply(this, arguments);
2177    }
2178
2179    Class.prototype = proto; //noinspection JSUnresolvedVariable
2180
2181    Class.prototype.constructor = _is.fn(proto.__ctor__) ? proto.__ctor__ : Class;
2182    Class.extend = _.Class.extend;
2183    Class.override = _.Class.override;
2184    Class.getBaseClasses = _.Class.getBaseClasses;
2185    Class.__baseClass__ = this;
2186    return Class;
2187  };
2188  /**
2189   * @summary Overrides a single method on this class.
2190   * @memberof FooGallery.utils.Class.
2191   * @function override
2192   * @param {string} name - The name of the function to override.
2193   * @param {function} fn - The new function to override with, the `_super` method will be made available within this function.
2194   * @description This is a helper method for overriding a single function of a {@link FooGallery.utils.Class} or one of its child classes. This uses the {@link FooGallery.utils.fn.addOrOverride} method internally and simply provides the correct prototype.
2195   * @example {@caption The below example wraps the `Person.prototype.dance` method with a new one that inverts the result. Note the override applies even to instances of the class that are already created.}{@run true}
2196   * var Person = FooGallery.utils.Class.extend({
2197   *   construct: function(isDancing){
2198   *     this.dancing = isDancing;
2199   *   },
2200   *   dance: function(){
2201   *     return this.dancing;
2202   *   }
2203   * });
2204   *
2205   * var p = new Person(true);
2206   * console.log( p.dance() ); // => true
2207   *
2208   * Person.override("dance", function(){
2209   * 	// Call the original version of dance()
2210   * 	return !this._super();
2211   * });
2212   *
2213   * console.log( p.dance() ); // => false
2214   */
2215
2216
2217  _.Class.override = function (name, fn) {
2218    _fn.addOrOverride(this.prototype, name, fn);
2219  };
2220  /**
2221   * @summary The base class for this class.
2222   * @memberof FooGallery.utils.Class.
2223   * @name __baseClass__
2224   * @type {?FooGallery.utils.Class}
2225   * @private
2226   */
2227
2228
2229  _.Class.__baseClass__ = null;
2230
2231  function getBaseClasses(klass, result) {
2232    if (!_is.array(result)) result = [];
2233
2234    if (_is.fn(klass) && klass.__baseClass__ !== null) {
2235      result.unshift(klass.__baseClass__);
2236      return getBaseClasses(klass.__baseClass__, result);
2237    }
2238
2239    return result;
2240  }
2241  /**
2242   * @summary Get an array of all base classes for this class.
2243   * @memberof FooGallery.utils.Class.
2244   * @function getBaseClasses
2245   * @returns {FooGallery.utils.Class[]}
2246   */
2247
2248
2249  _.Class.getBaseClasses = function () {
2250    return getBaseClasses(this, []);
2251  };
2252})( // dependencies
2253FooGallery.utils.$, FooGallery.utils, FooGallery.utils.is, FooGallery.utils.obj, FooGallery.utils.fn);
2254
2255(function ($, _, _is, _fn, _obj) {
2256  /**
2257   * @summary A registry class allowing classes to be easily registered and created.
2258   * @memberof FooGallery.utils.
2259   * @class ClassRegistry
2260   * @param {FooGallery.utils.ClassRegistry~Options} [options] - The options for the registry.
2261   * @augments FooGallery.utils.Class
2262   * @borrows FooGallery.utils.Class.extend as extend
2263   * @borrows FooGallery.utils.Class.override as override
2264   * @borrows FooGallery.utils.Class.getBaseClasses as getBaseClasses
2265   */
2266  _.ClassRegistry = _.Class.extend(
2267  /** @lends FooGallery.utils.ClassRegistry.prototype */
2268  {
2269    /**
2270     * @ignore
2271     * @constructs
2272     * @param {FooGallery.utils.ClassRegistry~Options} [options] - The options for the registry.
2273     */
2274    construct: function construct(options) {
2275      var self = this;
2276      /**
2277       * @summary A callback allowing the arguments supplied to the constructor of a new class to be modified.
2278       * @callback FooGallery.utils.ClassRegistry~beforeCreate
2279       * @param {FooGallery.utils.ClassRegistry~RegisteredClass} registered - The registered object containing all the information for the class being created.
2280       * @param {Array} args - An array of all arguments to be supplied to the constructor of the new class.
2281       * @returns {Array} Returns an array of all arguments to be supplied to the constructor of the new class.
2282       * @this FooGallery.utils.ClassRegistry
2283       */
2284
2285      /**
2286       * @summary The options for the registry.
2287       * @typedef {?Object} FooGallery.utils.ClassRegistry~Options
2288       * @property {boolean} [allowBase] - Whether or not to allow base classes to be created. Base classes are registered with a priority below 0.
2289       * @property {?FooGallery.utils.ClassRegistry~beforeCreate} [beforeCreate] - A callback executed just prior to creating an instance of a registered class. This must return an array of arguments to supply to the constructor of the new class.
2290       */
2291
2292      /**
2293       * @summary The options for this instance.
2294       * @memberof FooGallery.utils.ClassRegistry#
2295       * @name opt
2296       * @type {FooGallery.utils.ClassRegistry~Options}
2297       */
2298
2299      self.opt = _obj.extend({
2300        allowBase: true,
2301        beforeCreate: null
2302      }, options);
2303      /**
2304       * @summary An object detailing a registered class.
2305       * @typedef {?Object} FooGallery.utils.ClassRegistry~RegisteredClass
2306       * @property {string} name - The name of the class.
2307       * @property {FooGallery.utils.Class} ctor - The class constructor.
2308       * @property {string} selector - The CSS selector for the class.
2309       * @property {Object} config - The configuration object for the class providing default values that can be overridden at runtime.
2310       * @property {number} priority - This determines the index for the class when using the {@link FooGallery.utils.ClassRegistry#find|find} method, a higher value equals a lower index.
2311       */
2312
2313      /**
2314       * @summary An object containing all registered classes.
2315       * @memberof FooGallery.utils.ClassRegistry#
2316       * @name registered
2317       * @type {Object.<string, FooGallery.utils.ClassRegistry~RegisteredClass>}
2318       * @readonly
2319       * @example {@caption The following shows the structure of this object. The `<name>` placeholders would be the name the class was registered with.}
2320       * {
2321       * 	"<name>": {
2322       * 		"name": <string>,
2323       * 		"ctor": <function>,
2324       * 		"selector": <string>,
2325       * 		"config": <object>,
2326       * 		"priority": <number>
2327       * 	},
2328       * 	"<name>": {
2329       * 		"name": <string>,
2330       * 		"ctor": <function>,
2331       * 		"selector": <string>,
2332       * 		"config": <object>,
2333       * 		"priority": <number>
2334       * 	},
2335       * 	...
2336       * }
2337       */
2338
2339      self.registered = {};
2340    },
2341
2342    /**
2343     * @summary Register a class constructor with the provided `name`.
2344     * @memberof FooGallery.utils.ClassRegistry#
2345     * @function register
2346     * @param {string} name - The name of the class.
2347     * @param {FooGallery.utils.Class} klass - The class constructor to register.
2348     * @param {Object} [config] - The configuration object for the class providing default values that can be overridden at runtime.
2349     * @param {number} [priority=0] - This determines the index for the class when using the {@link FooGallery.utils.ClassRegistry#find|find} method, a higher value equals a lower index.
2350     * @returns {boolean} Returns `true` if the class was successfully registered.
2351     */
2352    register: function register(name, klass, config, priority) {
2353      var self = this;
2354
2355      if (_is.string(name) && !_is.empty(name) && _is.fn(klass)) {
2356        priority = _is.number(priority) ? priority : 0;
2357        var current = self.registered[name];
2358        self.registered[name] = {
2359          name: name,
2360          ctor: klass,
2361          config: _is.hash(config) ? config : {},
2362          priority: !_is.undef(current) ? current.priority : priority
2363        };
2364        return true;
2365      }
2366
2367      return false;
2368    },
2369
2370    /**
2371     * @summary The callback function for the {@link FooGallery.utils.ClassRegistry#each|each} method.
2372     * @callback FooGallery.utils.ClassRegistry~eachCallback
2373     * @param {FooGallery.utils.ClassRegistry~RegisteredClass} registered - The current registered class being iterated over.
2374     * @param {number} index - The array index of the `registered` object.
2375     * @returns {(boolean|undefined)} Return `false` to break out of the loop, all other values are ignored.
2376     */
2377
2378    /**
2379     * @summary Iterates over all registered classes executing the provided callback once per class.
2380     * @param {FooGallery.utils.ClassRegistry~eachCallback} callback - The callback to execute for each registered class.
2381     * @param {boolean} [prioritize=false] - Whether or not the registered classes should be prioritized before iteration.
2382     * @param {*} [thisArg] - The value of `this` within the callback.
2383     */
2384    each: function each(callback, prioritize, thisArg) {
2385      prioritize = _is.boolean(prioritize) ? prioritize : false;
2386      thisArg = _is.undef(thisArg) ? callback : thisArg;
2387      var self = this,
2388          names = Object.keys(self.registered),
2389          registered = names.map(function (name) {
2390        return self.registered[name];
2391      });
2392
2393      if (prioritize) {
2394        registered.sort(function (a, b) {
2395          return b.priority - a.priority;
2396        });
2397      }
2398
2399      var i = 0,
2400          l = registered.length;
2401
2402      for (; i < l; i++) {
2403        var result = callback.call(thisArg, registered[i], i);
2404        if (result === false) break;
2405      }
2406    },
2407
2408    /**
2409     * @summary The callback function for the {@link FooGallery.utils.ClassRegistry#find|find} method.
2410     * @callback FooGallery.utils.ClassRegistry~findCallback
2411     * @param {FooGallery.utils.ClassRegistry~RegisteredClass} registered - The current registered class being iterated over.
2412     * @param {number} index - The array index of the `registered` object.
2413     * @returns {boolean} `true` to return the current registered class.
2414     */
2415
2416    /**
2417     * @summary Iterates through all registered classes until the supplied `callback` returns a truthy value.
2418     * @param {FooGallery.utils.ClassRegistry~findCallback} callback - The callback to execute for each registered class.
2419     * @param {boolean} [prioritize=false] - Whether or not the registered classes should be prioritized before iteration.
2420     * @param {*} [thisArg] - The value of `this` within the callback.
2421     * @returns {?FooGallery.utils.ClassRegistry~RegisteredClass} `null` if no registered class satisfied the `callback`.
2422     */
2423    find: function find(callback, prioritize, thisArg) {
2424      prioritize = _is.boolean(prioritize) ? prioritize : false;
2425      thisArg = _is.undef(thisArg) ? callback : thisArg;
2426      var self = this,
2427          names = Object.keys(self.registered),
2428          registered = names.map(function (name) {
2429        return self.registered[name];
2430      });
2431
2432      if (prioritize) {
2433        registered.sort(function (a, b) {
2434          return b.priority - a.priority;
2435        });
2436      }
2437
2438      var i = 0,
2439          l = registered.length;
2440
2441      for (; i < l; i++) {
2442        if (callback.call(thisArg, registered[i], i)) {
2443          return registered[i];
2444        }
2445      }
2446
2447      return null;
2448    },
2449
2450    /**
2451     * @summary Create a new instance of a registered class by `name`.
2452     * @memberof FooGallery.utils.ClassRegistry#
2453     * @function create
2454     * @param {string} name - The name of the class to create.
2455     * @param {Object} [config] - Any custom configuration to supply to the class.
2456     * @param {...*} [argN] - Any number of additional arguments to pass to the class constructor.
2457     * @returns {?FooGallery.utils.Class} Returns `null` if no registered class can handle the supplied `element`.
2458     */
2459    create: function create(name, config, argN) {
2460      var self = this,
2461          args = _fn.arg2arr(arguments);
2462
2463      name = args.shift();
2464
2465      if (_is.string(name) && self.registered.hasOwnProperty(name)) {
2466        var registered = self.registered[name];
2467        var allowed = true;
2468        if (registered.priority < 0 && !self.opt.allowBase) allowed = false;
2469
2470        if (allowed && _is.fn(registered.ctor)) {
2471          config = args.shift();
2472          config = self.mergeConfigurations(registered.name, config);
2473          args.unshift.apply(args, [registered.name, config]);
2474          return _fn.apply(registered.ctor, self.onBeforeCreate(registered, args));
2475        }
2476      }
2477
2478      return null;
2479    },
2480
2481    /**
2482     * @summary Executes the beforeCreate callback if supplied and gives sub-classes an easy way to modify the arguments supplied to newly created classes.
2483     * @memberof FooGallery.utils.ClassRegistry#
2484     * @function onBeforeCreate
2485     * @param {FooGallery.utils.ClassRegistry~RegisteredClass} registered - The registered class about to be created.
2486     * @param {Array} args - The array of arguments to be supplied to the registered class constructor.
2487     * @returns {Array}
2488     */
2489    onBeforeCreate: function onBeforeCreate(registered, args) {
2490      var self = this;
2491
2492      if (self.opt.beforeCreate !== null && _is.fn(self.opt.beforeCreate)) {
2493        return self.opt.beforeCreate.call(self, registered, args);
2494      }
2495
2496      return args;
2497    },
2498
2499    /**
2500     * @summary Get the merged configuration for a class.
2501     * @memberof FooGallery.utils.ClassRegistry#
2502     * @function mergeConfigurations
2503     * @param {string} name - The name of the class to get the config for.
2504     * @param {Object} [config] - The user supplied defaults to override.
2505     * @returns {Object}
2506     */
2507    mergeConfigurations: function mergeConfigurations(name, config) {
2508      var self = this;
2509
2510      if (_is.string(name) && self.registered.hasOwnProperty(name)) {
2511        // check params
2512        config = _is.hash(config) ? config : {};
2513        var baseClasses = self.getBaseClasses(name),
2514            eArgs = [{}];
2515        baseClasses.push(self.registered[name]);
2516        baseClasses.forEach(function (reg) {
2517          eArgs.push(reg.config);
2518        });
2519        eArgs.push(config);
2520        return _obj.extend.apply(_obj, eArgs);
2521      }
2522
2523      return {};
2524    },
2525
2526    /**
2527     * @summary Gets the registered base class for this instance.
2528     * @memberof FooGallery.utils.ClassRegistry#
2529     * @function getBaseClass
2530     * @returns {?FooGallery.utils.ClassRegistry~RegisteredClass}
2531     */
2532    getBaseClass: function getBaseClass() {
2533      return this.find(function (registered) {
2534        return registered.priority < 0;
2535      }, true);
2536    },
2537
2538    /**
2539     * @summary Get all registered base classes for the supplied `name`.
2540     * @memberof FooGallery.utils.ClassRegistry#
2541     * @function getBaseClasses
2542     * @param {string} name - The name of the class to get the base classes for.
2543     * @returns {FooGallery.utils.ClassRegistry~RegisteredClass[]}
2544     */
2545    getBaseClasses: function getBaseClasses(name) {
2546      var self = this,
2547          reg = self.registered[name],
2548          result = [];
2549
2550      if (!_is.undef(reg)) {
2551        reg.ctor.getBaseClasses().forEach(function (base) {
2552          var found = self.fromType(base);
2553
2554          if (_is.hash(found)) {
2555            result.push(found);
2556          }
2557        });
2558      }
2559
2560      return result;
2561    },
2562
2563    /**
2564     * @summary Attempts to find a registered class given the type/constructor.
2565     * @memberof FooGallery.utils.ClassRegistry#
2566     * @function fromType
2567     * @param {FooGallery.utils.Class} type - The type/constructor of the registered class to find.
2568     * @returns {(FooGallery.utils.ClassRegistry~RegisteredClass|undefined)} Returns the registered class if found. Otherwise, `undefined` is returned.
2569     */
2570    fromType: function fromType(type) {
2571      if (!_is.fn(type)) return;
2572      return this.find(function (registered) {
2573        return registered.ctor === type;
2574      });
2575    }
2576  });
2577})(FooGallery.utils.$, FooGallery.utils, FooGallery.utils.is, FooGallery.utils.fn, FooGallery.utils.obj);
2578
2579(function (_, _is, _str) {
2580  // only register methods if this version is the current version
2581  if (_.version !== '1.0.1') return; // noinspection JSUnusedGlobalSymbols
2582
2583  /**
2584   * @summary A base event class providing just a type and defaultPrevented properties.
2585   * @memberof FooGallery.utils.
2586   * @class Event
2587   * @param {string} type - The type for this event.
2588   * @augments FooGallery.utils.Class
2589   * @borrows FooGallery.utils.Class.extend as extend
2590   * @borrows FooGallery.utils.Class.override as override
2591   * @description This is a very basic event class that is used internally by the {@link FooGallery.utils.EventClass#trigger} method when the first parameter supplied is simply the event name.
2592   *
2593   * To trigger your own custom event you will need to inherit from this class and then supply the instantiated event object as the first parameter to the {@link FooGallery.utils.EventClass#trigger} method.
2594   * @example {@caption The following shows how to use this class to create a custom event.}
2595   * var MyEvent = FooGallery.utils.Event.extend({
2596   * 	construct: function(type, customProp){
2597   * 	    this._super(type);
2598   * 	    this.myCustomProp = customProp;
2599   * 	}
2600   * });
2601   *
2602   * // to use the class you would then instantiate it and pass it as the first argument to a FooGallery.utils.EventClass's trigger method
2603   * var eventClass = ...; // any class inheriting from FooGallery.utils.EventClass
2604   * var event = new MyEvent( "my-event-type", true );
2605   * eventClass.trigger(event);
2606   */
2607
2608  _.Event = _.Class.extend(
2609  /** @lends FooGallery.utils.Event.prototype */
2610  {
2611    /**
2612     * @ignore
2613     * @constructs
2614     * @param {string} type
2615     **/
2616    construct: function construct(type) {
2617      if (_is.empty(type)) throw new SyntaxError('FooGallery.utils.Event objects must be supplied a `type`.');
2618
2619      var self = this,
2620          parsed = _.Event.parse(type);
2621      /**
2622       * @summary The type of event.
2623       * @memberof FooGallery.utils.Event#
2624       * @name type
2625       * @type {string}
2626       * @readonly
2627       */
2628
2629
2630      self.type = parsed.type;
2631      /**
2632       * @summary The namespace of the event.
2633       * @memberof FooGallery.utils.Event#
2634       * @name namespace
2635       * @type {string}
2636       * @readonly
2637       */
2638
2639      self.namespace = parsed.namespace;
2640      /**
2641       * @summary Whether the default action should be taken or not.
2642       * @memberof FooGallery.utils.Event#
2643       * @name defaultPrevented
2644       * @type {boolean}
2645       * @readonly
2646       */
2647
2648      self.defaultPrevented = false;
2649      /**
2650       * @summary The original {@link FooGallery.utils.EventClass} that triggered this event.
2651       * @memberof FooGallery.utils.Event#
2652       * @name target
2653       * @type {FooGallery.utils.EventClass}
2654       */
2655
2656      self.target = null;
2657    },
2658
2659    /**
2660     * @summary Informs the class that raised this event that its default action should not be taken.
2661     * @memberof FooGallery.utils.Event#
2662     * @function preventDefault
2663     */
2664    preventDefault: function preventDefault() {
2665      this.defaultPrevented = true;
2666    },
2667
2668    /**
2669     * @summary Gets whether the default action should be taken or not.
2670     * @memberof FooGallery.utils.Event#
2671     * @function isDefaultPrevented
2672     * @returns {boolean}
2673     */
2674    isDefaultPrevented: function isDefaultPrevented() {
2675      return this.defaultPrevented;
2676    }
2677  });
2678  /**
2679   * @summary Parse the provided event string into a type and namespace.
2680   * @memberof FooGallery.utils.Event.
2681   * @function parse
2682   * @param {string} event - The event to parse.
2683   * @returns {{namespaced: boolean, type: string, namespace: string}}
2683 Returns an object containing the type and namespace for the event.
2684   */
2685
2686  _.Event.parse = function (event) {
2687    event = _is.string(event) && !_is.empty(event) ? event : null;
2688
2689    var namespaced = _str.contains(event, ".");
2690
2691    return {
2692      namespaced: namespaced,
2693      type: namespaced ? _str.startsWith(event, ".") ? null : _str.until(event, ".") : event,
2694      namespace: namespaced ? _str.from(event, ".") : null
2695    };
2696  }; // noinspection JSUnusedGlobalSymbols
2697
2698  /**
2699   * @summary A base class that implements a basic events interface.
2700   * @memberof FooGallery.utils.
2701   * @class EventClass
2702   * @augments FooGallery.utils.Class
2703   * @borrows FooGallery.utils.Class.extend as extend
2704   * @borrows FooGallery.utils.Class.override as override
2705   * @description This is a very basic events implementation that provides just enough to cover most needs.
2706   */
2707
2708
2709  _.EventClass = _.Class.extend(
2710  /** @lends FooGallery.utils.EventClass.prototype */
2711  {
2712    /**
2713     * @ignore
2714     * @constructs
2715     **/
2716    construct: function construct() {
2717      /**
2718       * @summary An object containing all the required info to execute a listener.
2719       * @typedef {Object} FooGallery.utils.EventClass~RegisteredListener
2720       * @property {string} namespace - The namespace for the listener.
2721       * @property {function} fn - The callback function for the listener.
2722       * @property {*} thisArg - The `this` value to execute the callback with.
2723       */
2724
2725      /**
2726       * @summary An object containing a mapping of events to listeners.
2727       * @typedef {Object.<string, Array<FooGallery.utils.EventClass~RegisteredListener>>} FooGallery.utils.EventClass~RegisteredEvents
2728       */
2729
2730      /**
2731       * @summary The object used to register event handlers.
2732       * @memberof FooGallery.utils.EventClass#
2733       * @name events
2734       * @type {FooGallery.utils.EventClass~RegisteredEvents}
2735       */
2736      this.events = {};
2737    },
2738
2739    /**
2740     * @summary Destroy the current instance releasing used resources.
2741     * @memberof FooGallery.utils.EventClass#
2742     * @function destroy
2743     */
2744    destroy: function destroy() {
2745      this.events = {};
2746    },
2747
2748    /**
2749     * @summary Attach multiple event listeners to the class.
2750     * @memberof FooGallery.utils.EventClass#
2751     * @function on
2752     * @param {Object.<string, function>} events - An object containing event types to listener mappings.
2753     * @param {*} [thisArg] - The value of `this` within the listeners. Defaults to the class raising the event.
2754     * @returns {this}
2755     */
2756
2757    /**
2758    * @summary Attach an event listener for one or more events to the class.
2759    * @memberof FooGallery.utils.EventClass#
2760    * @function on
2761    * @param {string} events - One or more space-separated event types.
2762    * @param {function} listener - A function to execute when the event is triggered.
2763    * @param {*} [thisArg] - The value of `this` within the `listener`. Defaults to the class raising the event.
2764    * @returns {this}
2765    */
2766    on: function on(events, listener, thisArg) {
2767      var self = this;
2768
2769      if (_is.object(events)) {
2770        thisArg = listener;
2771        Object.keys(events).forEach(function (key) {
2772          if (_is.fn(events[key])) {
2773            key.split(" ").forEach(function (type) {
2774              self.addListener(type, events[key], thisArg);
2775            });
2776          }
2777        });
2778      } else if (_is.string(events) && _is.fn(listener)) {
2779        events.split(" ").forEach(function (type) {
2780          self.addListener(type, listener, thisArg);
2781        });
2782      }
2783
2784      return self;
2785    },
2786
2787    /**
2788     * @summary Adds a single event listener to the current class.
2789     * @memberof FooGallery.utils.EventClass#
2790     * @function addListener
2791     * @param {string} event - The event type, this can not contain any whitespace.
2792     * @param {function} listener - A function to execute when the event is triggered.
2793     * @param {*} [thisArg] - The value of `this` within the `listener`. Defaults to the class raising the event.
2794     * @returns {boolean} Returns `true` if added.
2795     */
2796    addListener: function addListener(event, listener, thisArg) {
2797      if (!_is.string(event) || /\s/.test(event) || !_is.fn(listener)) return false;
2798
2799      var self = this,
2800          parsed = _.Event.parse(event);
2801
2802      thisArg = _is.undef(thisArg) ? self : thisArg;
2803
2804      if (!_is.array(self.events[parsed.type])) {
2805        self.events[parsed.type] = [];
2806      }
2807
2808      var exists = self.events[parsed.type].some(function (h) {
2809        return h.namespace === parsed.namespace && h.fn === listener && h.thisArg === thisArg;
2810      });
2811
2812      if (!exists) {
2813        self.events[parsed.type].push({
2814          namespace: parsed.namespace,
2815          fn: listener,
2816          thisArg: thisArg
2817        });
2818        return true;
2819      }
2820
2821      return false;
2822    },
2823
2824    /**
2825     * @summary Remove multiple event listeners from the class.
2826     * @memberof FooGallery.utils.EventClass#
2827     * @function off
2828     * @param {Object.<string, function>} events - An object containing event types to listener mappings.
2829     * @param {*} [thisArg] - The value of `this` within the `listener` function. Defaults to the class raising the event.
2830     * @returns {this}
2831     */
2832
2833    /**
2834    * @summary Remove an event listener from the class.
2835    * @memberof FooGallery.utils.EventClass#
2836    * @function off
2837    * @param {string} events - One or more space-separated event types.
2838    * @param {function} listener - A function to execute when the event is triggered.
2839    * @param {*} [thisArg] - The value of `this` within the `listener`. Defaults to the class raising the event.
2840    * @returns {this}
2841    */
2842    off: function off(events, listener, thisArg) {
2843      var self = this;
2844
2845      if (_is.object(events)) {
2846        thisArg = listener;
2847        Object.keys(events).forEach(function (key) {
2848          key.split(" ").forEach(function (type) {
2849            self.removeListener(type, events[key], thisArg);
2850          });
2851        });
2852      } else if (_is.string(events)) {
2853        events.split(" ").forEach(function (type) {
2854          self.removeListener(type, listener, thisArg);
2855        });
2856      }
2857
2858      return self;
2859    },
2860
2861    /**
2862     * @summary Removes a single event listener from the current class.
2863     * @memberof FooGallery.utils.EventClass#
2864     * @function removeListener
2865     * @param {string} event - The event type, this can not contain any whitespace.
2866     * @param {function} [listener] - The listener registered to the event type.
2867     * @param {*} [thisArg] - The value of `this` registered for the `listener`. Defaults to the class raising the event.
2868     * @returns {boolean} Returns `true` if removed.
2869     */
2870    removeListener: function removeListener(event, listener, thisArg) {
2871      if (!_is.string(event) || /\s/.test(event)) return false;
2872
2873      var self = this,
2874          parsed = _.Event.parse(event),
2875          types = [];
2876
2877      thisArg = _is.undef(thisArg) ? self : thisArg;
2878
2879      if (!_is.empty(parsed.type)) {
2880        types.push(parsed.type);
2881      } else if (!_is.empty(parsed.namespace)) {
2882        types.push.apply(types, Object.keys(self.events));
2883      }
2884
2885      types.forEach(function (type) {
2886        if (!_is.array(self.events[type])) return;
2887        self.events[type] = self.events[type].filter(function (h) {
2888          if (listener != null) {
2889            return !(h.namespace === parsed.namespace && h.fn === listener && h.thisArg === thisArg);
2890          }
2891
2892          if (parsed.namespace != null) {
2893            return h.namespace !== parsed.namespace;
2894          }
2895
2896          return false;
2897        });
2898
2899        if (self.events[type].length === 0) {
2900          delete self.events[type];
2901        }
2902      });
2903      return true;
2904    },
2905
2906    /**
2907     * @summary Trigger an event on the current class.
2908     * @memberof FooGallery.utils.EventClass#
2909     * @function trigger
2910     * @param {(string|FooGallery.utils.Event)} event - Either a space-separated string of event types or a custom event object to raise.
2911     * @param {Array} [args] - An array of additional arguments to supply to the listeners after the event object.
2912     * @returns {(FooGallery.utils.Event|FooGallery.utils.Event[]|null)} Returns the {@link FooGallery.utils.Event|event object} of the triggered event. If more than one event was triggered an array of {@link FooGallery.utils.Event|event objects} is returned. If no `event` was supplied or triggered `null` is returned.
2913     */
2914    trigger: function trigger(event, args) {
2915      args = _is.array(args) ? args : [];
2916      var self = this,
2917          result = [];
2918
2919      if (event instanceof _.Event) {
2920        result.push(event);
2921        self.emit(event, args);
2922      } else if (_is.string(event)) {
2923        event.split(" ").forEach(function (type) {
2924          var e = new _.Event(type);
2925          result.push(e);
2926          self.emit(e, args);
2927        });
2928      }
2929
2930      return _is.empty(result) ? null : result.length === 1 ? result[0] : result;
2931    },
2932
2933    /**
2934     * @summary Emits the supplied event on the current class.
2935     * @memberof FooGallery.utils.EventClass#
2936     * @function emit
2937     * @param {FooGallery.utils.Event} event - The event object to emit.
2938     * @param {Array} [args] - An array of additional arguments to supply to the listener after the event object.
2939     */
2940    emit: function emit(event, args) {
2941      if (!(event instanceof FooGallery.utils.Event)) return;
2942      var self = this;
2943      args = _is.array(args) ? args : [];
2944      if (event.target === null) event.target = self;
2945
2946      if (_is.array(self.events[event.type])) {
2947        self.events[event.type].forEach(function (h) {
2948          if (event.namespace != null && h.namespace !== event.namespace) return;
2949          h.fn.apply(h.thisArg, [event].concat(args));
2950        });
2951      }
2952
2953      if (_is.array(self.events["__all__"])) {
2954        self.events["__all__"].forEach(function (h) {
2955          h.fn.apply(h.thisArg, [event].concat(args));
2956        });
2957      }
2958    }
2959  });
2960})( // dependencies
2961FooGallery.utils, FooGallery.utils.is, FooGallery.utils.str);
2962
2963(function ($, _, _is, _fn, _obj) {
2964  // only register methods if this version is the current version
2965  if (_.version !== '1.0.1') return;
2966  /**
2967   * @summary A simple timer that triggers events.
2968   * @memberof FooGallery.utils.
2969   * @class Timer
2970   * @param {number}
2970 [interval=1000] - The internal tick interval of the timer.
2971   */
2972
2973  _.Timer = _.EventClass.extend(
2974  /** @lends FooGallery.utils.Timer */
2975  {
2976    /**
2977     * @ignore
2978     * @constructs
2979     * @param {number} [interval=1000]
2980     */
2981    construct: function construct(interval) {
2982      var self = this;
2983
2984      self._super();
2985      /**
2986       * @summary The internal tick interval of the timer in milliseconds.
2987       * @memberof FooGallery.utils.Timer#
2988       * @name interval
2989       * @type {number}
2990       * @default 1000
2991       * @readonly
2992       */
2993
2994
2995      self.interval = _is.number(interval) ? interval : 1000;
2996      /**
2997       * @summary Whether the timer is currently running or not.
2998       * @memberof FooGallery.utils.Timer#
2999       * @name isRunning
3000       * @type {boolean}
3001       * @default false
3002       * @readonly
3003       */
3004
3005      self.isRunning = false;
3006      /**
3007       * @summary Whether the timer is currently paused or not.
3008       * @memberof FooGallery.utils.Timer#
3009       * @name isPaused
3010       * @type {boolean}
3011       * @default false
3012       * @readonly
3013       */
3014
3015      self.isPaused = false;
3016      /**
3017       * @summary Whether the timer can resume from a previous state or not.
3018       * @memberof FooGallery.utils.Timer#
3019       * @name canResume
3020       * @type {boolean}
3021       * @default false
3022       * @readonly
3023       */
3024
3025      self.canResume = false;
3026      /**
3027       * @summary Whether the timer can restart or not.
3028       * @memberof FooGallery.utils.Timer#
3029       * @name canRestart
3030       * @type {boolean}
3031       * @default false
3032       * @readonly
3033       */
3034
3035      self.canRestart = false;
3036      /**
3037       * @summary The internal tick timeout ID.
3038       * @memberof FooGallery.utils.Timer#
3039       * @name __timeout
3040       * @type {?number}
3041       * @default null
3042       * @private
3043       */
3044
3045      self.__timeout = null;
3046      /**
3047       * @summary Whether the timer is incrementing or decrementing.
3048       * @memberof FooGallery.utils.Timer#
3049       * @name __decrement
3050       * @type {boolean}
3051       * @default false
3052       * @private
3053       */
3054
3055      self.__decrement = false;
3056      /**
3057       * @summary The total time for the timer.
3058       * @memberof FooGallery.utils.Timer#
3059       * @name __time
3060       * @type {number}
3061       * @default 0
3062       * @private
3063       */
3064
3065      self.__time = 0;
3066      /**
3067       * @summary The remaining time for the timer.
3068       * @memberof FooGallery.utils.Timer#
3069       * @name __remaining
3070       * @type {number}
3071       * @default 0
3072       * @private
3073       */
3074
3075      self.__remaining = 0;
3076      /**
3077       * @summary The current time for the timer.
3078       * @memberof FooGallery.utils.Timer#
3079       * @name __current
3080       * @type {number}
3081       * @default 0
3082       * @private
3083       */
3084
3085      self.__current = 0;
3086      /**
3087       * @summary The final time for the timer.
3088       * @memberof FooGallery.utils.Timer#
3089       * @name __finish
3090       * @type {number}
3091       * @default 0
3092       * @private
3093       */
3094
3095      self.__finish = 0;
3096      /**
3097       * @summary The last arguments supplied to the {@link FooGallery.utils.Timer#start|start} method.
3098       * @memberof FooGallery.utils.Timer#
3099       * @name __restart
3100       * @type {Array}
3101       * @default []
3102       * @private
3103       */
3104
3105      self.__restart = [];
3106    },
3107
3108    /**
3109     * @summary Resets the timer back to a fresh starting state.
3110     * @memberof FooGallery.utils.Timer#
3111     * @function __reset
3112     * @private
3113     */
3114    __reset: function __reset() {
3115      var self = this;
3116      clearTimeout(self.__timeout);
3117      self.__timeout = null;
3118      self.__decrement = false;
3119      self.__time = 0;
3120      self.__remaining = 0;
3121      self.__current = 0;
3122      self.__finish = 0;
3123      self.isRunning = false;
3124      self.isPaused = false;
3125      self.canResume = false;
3126    },
3127
3128    /**
3129     * @summary Generates event args to be passed to listeners of the timer events.
3130     * @memberof FooGallery.utils.Timer#
3131     * @function __eventArgs
3132     * @param {...*} [args] - Any number of additional arguments to pass to an event listener.
3133     * @return {Array} - The first 3 values of the result will always be the current time, the total time and boolean indicating if the timer is decremental.
3134     * @private
3135     */
3136    __eventArgs: function __eventArgs(args) {
3137      var self = this;
3138      return [self.__current, self.__time, self.__decrement].concat(_fn.arg2arr(arguments));
3139    },
3140
3141    /**
3142     * @summary Performs the tick for the timer checking and modifying the various internal states.
3143     * @memberof FooGallery.utils.Timer#
3144     * @function __tick
3145     * @private
3146     */
3147    __tick: function __tick() {
3148      var self = this;
3149      self.trigger("tick", self.__eventArgs());
3150
3151      if (self.__current === self.__finish) {
3152        self.trigger("complete", self.__eventArgs());
3153
3154        self.__reset();
3155      } else {
3156        if (self.__decrement) {
3157          self.__current--;
3158        } else {
3159          self.__current++;
3160        }
3161
3162        self.__remaining--;
3163        self.canResume = self.__remaining > 0;
3164        self.__timeout = setTimeout(function () {
3165          self.__tick();
3166        }, self.interval);
3167      }
3168    },
3169
3170    /**
3171     * @summary Starts the timer using the supplied `time` and whether or not to increment or decrement from the value.
3172     * @memberof FooGallery.utils.Timer#
3173     * @function start
3174     * @param {number} time - The total time in seconds for the timer.
3175     * @param {boolean} [decrement=false] - Whether the timer should increment or decrement from or to the supplied time.
3176     */
3177    start: function start(time, decrement) {
3178      var self = this;
3179      if (self.isRunning) return;
3180      decrement = _is.boolean(decrement) ? decrement : false;
3181      self.__restart = [time, decrement];
3182      self.__decrement = decrement;
3183      self.__time = time;
3184      self.__remaining = time;
3185      self.__current = decrement ? time : 0;
3186      self.__finish = decrement ? 0 : time;
3187      self.canRestart = true;
3188      self.isRunning = true;
3189      self.isPaused = false;
3190      self.trigger("start", self.__eventArgs());
3191
3192      self.__tick();
3193    },
3194
3195    /**
3196     * @summary Starts the timer counting down to `0` from the supplied `time`.
3197     * @memberof FooGallery.utils.Timer#
3198     * @function countdown
3199     * @param {number} time - The total time in seconds for the timer.
3200     */
3201    countdown: function countdown(time) {
3202      this.start(time, true);
3203    },
3204
3205    /**
3206     * @summary Starts the timer counting up from `0` to the supplied `time`.
3207     * @memberof FooGallery.utils.Timer#
3208     * @function countup
3209     * @param {number} time - The total time in seconds for the timer.
3210     */
3211    countup: function countup(time) {
3212      this.start(time, false);
3213    },
3214
3215    /**
3216     * @summary Stops and then restarts the timer using the last arguments supplied to the {@link FooGallery.utils.Timer#start|start} method.
3217     * @memberof FooGallery.utils.Timer#
3218     * @function restart
3219     */
3220    restart: function restart() {
3221      var self = this;
3222      self.stop();
3223
3224      if (self.canRestart) {
3225        self.start.apply(self, self.__restart);
3226      }
3227    },
3228
3229    /**
3230     * @summary Stops the timer.
3231     * @memberof FooGallery.utils.Timer#
3232     * @function stop
3233     */
3234    stop: function stop() {
3235      var self = this;
3236
3237      if (self.isRunning || self.isPaused) {
3238        self.__reset();
3239
3240        self.trigger("stop", self.__eventArgs());
3241      }
3242    },
3243
3244    /**
3245     * @summary Pauses the timer and returns the remaining seconds.
3246     * @memberof FooGallery.utils.Timer#
3247     * @function pause
3248     * @return {number} - The number of seconds remaining for the timer.
3249     */
3250    pause: function pause() {
3251      var self = this;
3252
3253      if (self.__timeout != null) {
3254        clearTimeout(self.__timeout);
3255        self.__timeout = null;
3256      }
3257
3258      if (self.isRunning) {
3259        self.isRunning = false;
3260        self.isPaused = true;
3261        self.trigger("pause", self.__eventArgs());
3262      }
3263
3264      return self.__remaining;
3265    },
3266
3267    /**
3268     * @summary Resumes the timer from a previously paused state.
3269     * @memberof FooGallery.utils.Timer#
3270     * @function resume
3271     */
3272    resume: function resume() {
3273      var self = this;
3274
3275      if (self.canResume) {
3276        self.isRunning = true;
3277        self.isPaused = false;
3278        self.trigger("resume", self.__eventArgs());
3279
3280        self.__tick();
3281      }
3282    },
3283
3284    /**
3285     * @summary Resets the timer back to a fresh starting state.
3286     * @memberof FooGallery.utils.Timer#
3287     * @function reset
3288     */
3289    reset: function reset() {
3290      var self = this;
3291
3292      self.__reset();
3293
3294      self.trigger("reset", this.__eventArgs());
3295    }
3296  });
3297})(FooGallery.utils.$, FooGallery.utils, FooGallery.utils.is, FooGallery.utils.fn, FooGallery.utils.obj);
3298
3299(function ($, _, _fn) {
3300  // only register methods if this version is the current version
3301  if (_.version !== '1.0.1') return; // noinspection JSUnusedGlobalSymbols
3302
3303  /**
3304   * @summary A wrapper around the fullscreen API to ensure cross browser compatibility.
3305   * @memberof FooGallery.utils.
3306   * @class FullscreenAPI
3307   * @augments FooGallery.utils.EventClass
3308   * @borrows FooGallery.utils.EventClass.extend as extend
3309   * @borrows FooGallery.utils.EventClass.override as override
3310   */
3311
3312  _.FullscreenAPI = _.EventClass.extend(
3313  /** @lends FooGallery.utils.FullscreenAPI */
3314  {
3315    /**
3316     * @ignore
3317     * @constructs
3318     */
3319    construct: function construct() {
3320      this._super();
3321      /**
3322       * @summary An object containing a single browsers various methods and events needed for this wrapper.
3323       * @typedef {?Object} FooGallery.utils.FullscreenAPI~BrowserAPI
3324       * @property {string} enabled
3325       * @property {string} element
3326       * @property {string} request
3327       * @property {string} exit
3328       * @property {Object} events
3329       * @property {string} events.change
3330       * @property {string} events.error
3331       */
3332
3333      /**
3334       * @summary An object containing the supported fullscreen browser API's.
3335       * @typedef {Object.<string, FooGallery.utils.FullscreenAPI~BrowserAPI>} FooGallery.utils.FullscreenAPI~SupportedBrowsers
3336       */
3337
3338      /**
3339       * @summary Contains the various browser specific method and event names.
3340       * @memberof FooGallery.utils.FullscreenAPI#
3341       * @name apis
3342       * @type {FooGallery.utils.FullscreenAPI~SupportedBrowsers}
3343       */
3344
3345
3346      this.apis = {
3347        w3: {
3348          enabled: "fullscreenEnabled",
3349          element: "fullscreenElement",
3350          request: "requestFullscreen",
3351          exit: "exitFullscreen",
3352          events: {
3353            change: "fullscreenchange",
3354            error: "fullscreenerror"
3355          }
3356        },
3357        webkit: {
3358          enabled: "webkitFullscreenEnabled",
3359          element: "webkitCurrentFullScreenElement",
3360          request: "webkitRequestFullscreen",
3361          exit: "webkitExitFullscreen",
3362          events: {
3363            change: "webkitfullscreenchange",
3364            error: "webkitfullscreenerror"
3365          }
3366        },
3367        moz: {
3368          enabled: "mozFullScreenEnabled",
3369          element: "mozFullScreenElement",
3370          request: "mozRequestFullScreen",
3371          exit: "mozCancelFullScreen",
3372          events: {
3373            change: "mozfullscreenchange",
3374            error: "mozfullscreenerror"
3375          }
3376        },
3377        ms: {
3378          enabled: "msFullscreenEnabled",
3379          element: "msFullscreenElement",
3380          request: "msRequestFullscreen",
3381          exit: "msExitFullscreen",
3382          events: {
3383            change: "MSFullscreenChange",
3384            error: "MSFullscreenError"
3385          }
3386        }
3387      };
3388      /**
3389       * @summary The current browsers specific method and event names.
3390       * @memberof FooGallery.utils.FullscreenAPI#
3391       * @name api
3392       * @type {FooGallery.utils.FullscreenAPI~BrowserAPI}
3393       */
3394
3395      this.api = this.getAPI();
3396      /**
3397       * @summary Whether or not the fullscreen API is supported in the current browser.
3398       * @memberof FooGallery.utils.FullscreenAPI#
3399       * @name supported
3400       * @type {boolean}
3401       */
3402
3403      this.supported = this.api != null;
3404
3405      this.__listen();
3406    },
3407
3408    /**
3409     * @summary Destroys the current wrapper unbinding events and freeing up resources.
3410     * @memberof FooGallery.utils.FullscreenAPI#
3411     * @function destroy
3412     * @returns {boolean}
3413     */
3414    destroy: function destroy() {
3415      this.__stopListening();
3416
3417      return this._super();
3418    },
3419
3420    /**
3421     * @summary Fetches the correct API for the current browser.
3422     * @memberof FooGallery.utils.FullscreenAPI#
3423     * @function getAPI
3424     * @return {?FooGallery.utils.FullscreenAPI~BrowserAPI} Returns `null` if the fullscreen API is not supported.
3425     */
3426    getAPI: function getAPI() {
3427      for (var vendor in this.apis) {
3428        if (!this.apis.hasOwnProperty(vendor)) continue; // Check if document has the "enabled" property
3429
3430        if (this.apis[vendor].enabled in document) {
3431          // It seems this browser supports the fullscreen API
3432          return this.apis[vendor];
3433        }
3434      }
3435
3436      return null;
3437    },
3438
3439    /**
3440     * @summary Gets the current fullscreen element or null.
3441     * @memberof FooGallery.utils.FullscreenAPI#
3442     * @function element
3443     * @returns {?Element}
3444     */
3445    element: function element() {
3446      return this.supported ? document[this.api.element] : null;
3447    },
3448
3449    /**
3450     * @summary Requests the browser to place the specified element into fullscreen mode.
3451     * @memberof FooGallery.utils.FullscreenAPI#
3452     * @function request
3453     * @param {Element} element - The element to place into fullscreen mode.
3454     * @returns {Promise} A Promise which is resolved once the element is placed into fullscreen mode.
3455     */
3456    request: function request(element) {
3457      if (this.supported && !!element[this.api.request]) {
3458        var result = element[this.api.request]();
3459        return !result ? $.Deferred(this.__resolver(this.api.request)).promise() : result;
3460      }
3461
3462      return _fn.rejected;
3463    },
3464
3465    /**
3466     * @summary Requests that the browser switch from fullscreen mode back to windowed mode.
3467     * @memberof FooGallery.utils.FullscreenAPI#
3468     * @function exit
3469     * @returns {Promise} A Promise which is resolved once fullscreen mode is exited.
3470     */
3471    exit: function exit() {
3472      if (this.supported && !!this.element()) {
3473        var result = document[this.api.exit]();
3474        return !result ? $.Deferred(this.__resolver(this.api.exit)).promise() : result;
3475      }
3476
3477      return _fn.rejected;
3478    },
3479
3480    /**
3481     * @summary Toggles the supplied element between fullscreen and windowed modes.
3482     * @memberof FooGallery.utils.FullscreenAPI#
3483     * @function toggle
3484     * @param {Element} element - The element to switch between modes.
3485     * @returns {Promise} A Promise that is resolved once fullscreen mode is either entered or exited.
3486     */
3487    toggle: function toggle(element) {
3488      return !!this.element() ? this.exit() : this.request(element);
3489    },
3490
3491    /**
3492     * @summary Starts listening to the document level fullscreen events and triggers an abbreviated version on this class.
3493     * @memberof FooGallery.utils.FullscreenAPI#
3494     * @function __listen
3495     * @private
3496     */
3497    __listen: function __listen() {
3498      var self = this;
3499      if (!self.supported) return;
3500      $(document).on(self.api.events.change + ".utils", function () {
3501        self.trigger("change");
3502      }).on(self.api.events.error + ".utils", function () {
3503        self.trigger("error");
3504      });
3505    },
3506
3507    /**
3508     * @summary Stops listening to the document level fullscreen events.
3509     * @memberof FooGallery.utils.FullscreenAPI#
3510     * @function __stopListening
3511     * @private
3512     */
3513    __stopListening: function __stopListening() {
3514      var self = this;
3515      if (!self.supported) return;
3516      $(document).off(self.api.events.change + ".utils").off(self.api.events.error + ".utils");
3517    },
3518
3519    /**
3520     * @summary Creates a resolver function to patch browsers which do not return a Promise from there request and exit methods.
3521     * @memberof FooGallery.utils.FullscreenAPI#
3522     * @function __resolver
3523     * @param {string} method - The request or exit method the resolver is being created for.
3524     * @returns {FooGallery.utils.FullscreenAPI~resolver}
3525     * @private
3526     */
3527    __resolver: function __resolver(method) {
3528      var self = this;
3529      /**
3530       * @summary Binds to the fullscreen change and error events and resolves or rejects the supplied deferred accordingly.
3531       * @callback FooGallery.utils.FullscreenAPI~resolver
3532       * @param {jQuery.Deferred} def - The jQuery.Deferred object to resolve.
3533       */
3534
3535      return function resolver(def) {
3536        // Reject the promise if asked to exitFullscreen and there is no element currently in fullscreen
3537        if (method === self.api.exit && !!self.element()) {
3538          setTimeout(function () {
3539            // noinspection JSUnresolvedFunction
3540            def.reject(new TypeError());
3541          }, 1);
3542          return;
3543        } // When receiving an internal fullscreenchange event, fulfill the promise
3544
3545
3546        function change() {
3547          // noinspection JSUnresolvedFunction
3548          def.resolve();
3549          $(document).off(self.api.events.change, change).off(self.api.events.error, error);
3550        } // When receiving an internal fullscreenerror event, reject the promise
3551
3552
3553        function error() {
3554          // noinspection JSUnresolvedFunction
3555          def.reject(new TypeError());
3556          $(document).off(self.api.events.change, change).off(self.api.events.error, error);
3557        }
3558
3559        $(document).on(self.api.events.change, change).on(self.api.events.error, error);
3560      };
3561    }
3562  });
3563})(FooGallery.utils.$, FooGallery.utils, FooGallery.utils.fn);
3564
3565(function ($, _, _is, _fn) {
3566  // only register methods if this version is the current version
3567  if (_.version !== '1.0.1') return;
3568  /**
3569   * @summary Contains common utility methods and members for the CSS transition property.
3570   * @memberof FooGallery.utils.
3571   * @namespace transition
3572   */
3573
3574  _.transition = {};
3575  /**
3576   * @summary The data name used by transitions to ensure promises are resolved.
3577   * @memberof FooGallery.utils.transition.
3578   * @name dataName
3579   * @type {string}
3580   * @default "__foo-transition__"
3581   */
3582
3583  _.transition.dataName = '__foo-transition__';
3584  /**
3585   * @summary The CSS className used to disable transitions when using the {@link FooGallery.utils.transition.disable|disable} method instead of inline styles.
3586   * @memberof FooGallery.utils.transition.
3587   * @name disableClassName
3588   * @type {?string}
3589   * @default null
3590   */
3591
3592  _.transition.disableClassName = null;
3593  /**
3594   * @summary The global timeout used as a safety measure when using the {@link FooGallery.utils.transition.start|start} method. This can be overridden using the `timeout` parameter of the {@link FooGallery.utils.transition.start|start} method.
3595   * @memberof FooGallery.utils.transition.
3596   * @name timeout
3597   * @type {number}
3598   * @default 3000
3599   */
3600
3601  _.transition.timeout = 3000;
3602  /**
3603   * @summary Disable transitions temporarily on the provided element so changes can be made immediately within the callback.
3604   * @memberof FooGallery.utils.transition.
3605   * @function disable
3606   * @param {(jQuery|HTMLElement)} element - The element to disable transitions on.
3607   * @param {FooGallery.utils.transition~modifyFn} modifyFn - A function to execute while the elements transitions are disabled.
3608   */
3609
3610  _.transition.disable = function (element, modifyFn) {
3611    var $el = _is.jq(element) ? element : $(element);
3612
3613    if ($el.length > 0 && _is.fn(modifyFn)) {
3614      var el = $el.get(0),
3615          hasClass = _is.string(_.transition.disableClassName);
3616
3617      var restore = null;
3618      if (hasClass) $el.addClass(_.transition.disableClassName);else {
3619        restore = {
3620          value: el.style.getPropertyValue('transition'),
3621          priority: el.style.getPropertyPriority('transition')
3622        };
3623        el.style.setProperty('transition', 'none', 'important');
3624      }
3625      modifyFn.call(modifyFn, $el);
3626      $el.prop("offsetWidth");
3627      if (hasClass) $el.removeClass(_.transition.disableClassName);else {
3628        el.style.removeProperty('transition');
3629
3630        if (_is.string(restore.value) && restore.value.length > 0) {
3631          el.style.setProperty('transition', restore.value, restore.priority);
3632        }
3633      }
3634    }
3635  };
3636  /**
3637   * @summary Stop a transition started using the {@link FooGallery.utils.transition.start|start} method.
3638   * @memberof FooGallery.utils.transition.
3639   * @function stop
3640   * @param {(jQuery|HTMLElement)} element - The element to stop the transition on.
3641   * @returns {Promise}
3642   */
3643
3644
3645  _.transition.stop = function (element) {
3646    var d = $.Deferred(),
3647        $el = _is.jq(element) ? element : $(element);
3648
3649    if ($el.length > 0) {
3650      var current = $el.data(_.transition.dataName);
3651
3652      if (_is.promise(current)) {
3653        current.always(function () {
3654          // request the next frame to give the previous event unbinds time to settle
3655          _.requestFrame(function () {
3656            d.resolve($el);
3657          });
3658        }).reject(new Error("Transition cancelled."));
3659      } else {
3660        d.resolve($el);
3661      }
3662    } else {
3663      d.reject(new Error("Unable to stop transition. Make sure the element exists."));
3664    }
3665
3666    return d.promise();
3667  };
3668  /**
3669   * @summary Creates a new transition event listener ensuring the element and optionally the propertyName matches before executing the callback.
3670   * @memberof FooGallery.utils.transition.
3671   * @function createListener
3672   * @param {HTMLElement} element - The element being listened to.
3673   * @param {function(*): void} callback - The callback to execute once the element and optional propertyName are matched.
3674   * @param {?string} [propertyName=null] - The propertyName to match on the TransitionEvent object.
3675   * @returns {function(*): void}
3676   */
3677
3678
3679  _.transition.createListener = function (element, callback, propertyName) {
3680    var el = element,
3681        fn = callback,
3682        prop = propertyName,
3683        hasProp = _is.string(propertyName);
3684
3685    return function (event) {
3686      var evt = event.originalEvent instanceof TransitionEvent ? event.originalEvent : event;
3687      var matches = false;
3688
3689      if (evt.target === el) {
3690        matches = hasProp ? evt.propertyName === prop : true;
3691      }
3692
3693      if (matches) fn.apply(fn, _fn.arg2arr(arguments));
3694    };
3695  };
3696  /**
3697   * @summary Start a transition on an element returning a promise that is resolved once the transition ends.
3698   * @memberof FooGallery.utils.transition.
3699   * @function start
3700   * @param {(jQuery|HTMLElement)} element - The element to perform the transition on.
3701   * @param {FooGallery.utils.transition~modifyFn} triggerFn - The callback that triggers the transition on the element.
3702   * @param {?string} [propertyName] - A specific property name to wait for before resolving. If not supplied the first instance of the transitionend event will resolve the promise.
3703   * @param {number} [timeout] - A safety timeout to ensure the returned promise is finalized. If not supplied the value of the {@link FooGallery.utils.transition.timeout} property is used.
3704   * @returns {Promise}
3705   */
3706
3707
3708  _.transition.start = function (element, triggerFn, propertyName, timeout) {
3709    var d = $.Deferred(),
3710        $el = _is.jq(element) ? element : $(element);
3711
3712    if ($el.length > 0 && _is.fn(triggerFn)) {
3713      var el = $el.get(0); // first stop any active transitions
3714
3715      _.transition.stop($el).always(function () {
3716        // then setup the data object and event listeners for the new transition
3717        var listener = _.transition.createListener(el, function () {
3718          d.resolve($el);
3719        }, propertyName);
3720
3721        $el.data(_.transition.dataName, d).on("transitionend.foo-utils", listener).prop("offsetWidth");
3721 // force layout to ensure transitions on newly appended elements occur
3722        // request the next frame to give the event bindings time to settle
3723
3724        _.requestFrame(function () {
3725          // just in case a transition is cancelled by some other means and the transitionend event is never fired this
3726          // timeout ensures the returned promise is always finalized.
3727          var safety = setTimeout(function () {
3728            d.reject(new Error("Transition safety timeout triggered."));
3729          }, _is.number(timeout) ? timeout : _.transition.timeout); // we always want to cleanup after ourselves so clear the safety, remove the data object and unbind the events
3730
3731          d.always(function () {
3732            clearTimeout(safety);
3733            $el.removeData(_.transition.dataName).off("transitionend.foo-utils", listener);
3734          }); // now that everything is setup kick off the transition by calling the triggerFn
3735
3736          triggerFn.call(triggerFn, $el);
3737        });
3738      });
3739    } else {
3740      d.reject(new Error("Unable to perform transition. Make sure the element exists and a trigger function is supplied."));
3741    }
3742
3743    return d.promise();
3744  };
3745  /**
3746   * @summary Used to modify an element which has transitions optionally allowing the transition to occur or not.
3747   * @memberof FooGallery.utils.transition.
3748   * @function modify
3749   * @param {(jQuery|HTMLElement)} element - The element to perform the modifications to.
3750   * @param {FooGallery.utils.transition~modifyFn} modifyFn - The callback used to perform the modifications.
3751   * @param {boolean} [immediate=false] - Whether or not transitions should be allowed to execute and waited on. The default value of `false` means transitions are allowed and the promise will only resolve once there transitionend event has fired.
3752   * @param {?string} [propertyName=null] - A specific property name to wait for before resolving. If not supplied the first instance of the transitionend event will resolve the promise.
3753   * @returns {Promise} Returns a promise that is resolved once the modifications to the element have ended.
3754   */
3755
3756
3757  _.transition.modify = function (element, modifyFn, immediate, propertyName) {
3758    var $el = _is.jq(element) ? element : $(element);
3759
3760    if ($el.length > 0 && _is.fn(modifyFn)) {
3761      if (immediate) {
3762        _.transition.disable($el, modifyFn);
3763
3764        return _fn.resolve();
3765      }
3766
3767      return _.transition.start($el, modifyFn, propertyName);
3768    }
3769
3770    return _fn.reject(new Error("Unable to perform modification. Make sure the element exists and a modify function is supplied."));
3771  };
3772  /**
3773   * @summary Perform one or more modifications to the element such as setting inline styles or toggling classNames.
3774   * @callback FooGallery.utils.transition~modifyFn
3775   * @param {jQuery} $element - The jQuery object for the element to modify.
3776   */
3777
3778})( // dependencies
3779FooGallery.utils.$, FooGallery.utils, FooGallery.utils.is, FooGallery.utils.fn);
3780
3781(function ($, _, _utils, _is, _fn, _str) {
3782
3783	/**
3784	 * @summary The name to use when getting or setting an instance of a {@link FooGallery.Template|template} on an element using jQuery's `.data()` method.
3785	 * @memberof FooGallery
3786	 * @name DATA_TEMPLATE
3787	 * @type {string}
3788	 * @default "__FooGallery__"
3789	 */
3790	_.DATA_TEMPLATE = "__FooGallery__";
3791
3792	/**
3793	 * @summary The name to use when getting or setting an instance of a {@link FooGallery.Item|item} on an element using jQuery's `.data()` method.
3794	 * @memberof FooGallery
3795	 * @name DATA_ITEM
3796	 * @type {string}
3797	 * @default "__FooGalleryItem__"
3798	 */
3799	_.DATA_ITEM = "__FooGalleryItem__";
3800
3801	_.get = function(selector){
3802		return $(selector).data(_.DATA_TEMPLATE);
3803	};
3804
3805	_.init = function (options, element) {
3806		element = _is.jq(element) ? element : $(element);
3807		if (element.length > 0){
3808			var current = element.data(_.DATA_TEMPLATE);
3809			if (current instanceof _.Template) {
3810				return current.destroy(true).then(function(){
3811					var replacement = element.data(_.DATA_TEMPLATE);
3812					if (replacement instanceof _.Template) {
3813						return replacement.initialize();
3814					}
3815					var tmpl = _.template.make(options, element);
3816					return tmpl instanceof _.Template ? tmpl.initialize() : _fn.rejected;
3817				});
3818			}
3819		}
3820		var tmpl = _.template.make(options, element);
3821		return tmpl instanceof _.Template ? tmpl.initialize() : _fn.rejected;
3822	};
3823
3824	/**
3825	 * @summary Expose FooGallery as a jQuery plugin.
3826	 * @memberof external:"jQuery.fn"#
3827	 * @function foogallery
3828	 * @param {(object|string)} [options] - The options to supply to FooGallery or one of the supported method n
3828ames.
3829	 * @param {external:"jQuery.fn"~readyCallback} [ready] - A callback executed once each template initialized is ready.
3830	 * @returns {jQuery}
3831	 * @example {@caption The below shows using this method in its simplest form, initializing a template on pre-existing elements.}{@lang html}
3832	 * <!-- The container element for the template -->
3833	 * <div id="gallery-1" class="foogallery">
3834	 *   <!-- A single item -->
3835	 *   <div class="fg-item" data-id="[item.id]">
3836	 *     <div class="fg-item-inner">
3837	 *       <a class="fg-thumb" href="[item.href]">
3838	 *         <img class="fg-image" width="[item.width]" height="[item.height]"
3839	 *          title="[item.title]" alt="[item.description]"
3840	 *          data-src="[item.src]"
3841	 *          data-srcset="[item.srcset]" />
3842	 *         <!-- Optional caption markup -->
3843	 *         <div class="fg-caption">
3844	 *          <div class="fg-caption-inner">
3845	 *           <div class="fg-caption-title">[item.title]</div>
3846	 *           <div class="fg-caption-desc">[item.description]</div>
3847	 *          </div>
3848	 *         </div>
3849	 *       </a>
3850	 *     </div>
3851	 *   </div>
3852	 *   <!-- Any number of additional items -->
3853	 * </div>
3854	 * <script>
3855	 *  jQuery(function($){
3856	 * 		$("#gallery-1").foogallery();
3857	 * 	});
3858	 * </script>
3859	 * @example {@caption Options can be supplied directly to the `.foogallery()` method or by supplying them using the `data-foogallery` attribute. If supplied using the attribute the value must follow [valid JSON syntax](http://en.wikipedia.org/wiki/JSON#Data_types.2C_syntax_and_example) including quoted property names. If the same option is supplied in both locations as it is below, the value from the attribute overrides the value supplied to the method, in this case `lazy` would be `true`.}{@lang html}
3860	 * <!-- Supplying the options using the attribute -->
3861	 * <div id="gallery-1" class="foogallery fg-responsive" data-foogallery='{"lazy": true}'>
3862	 *  <!-- Items -->
3863	 * </div>
3864	 * <script>
3865	 *  jQuery(function($){
3866	 * 		// Supply the options directly to the method
3867	 * 		$("#gallery-1").foogallery({
3868	 * 			lazy: false
3869	 * 		});
3870	 * 	});
3871	 * </script>
3872	 */
3873	$.fn.foogallery = function (options, ready) {
3874		ready = _is.fn(ready) ? ready : $.noop;
3875		return this.each(function (i, element) {
3876			if (_is.string(options)) {
3877				var template = $.data(element, _.DATA_TEMPLATE);
3878				if (template instanceof _.Template) {
3879					switch (options) {
3880						case "layout":
3881							template.layout();
3882							return;
3883						case "destroy":
3884							template.destroy();
3885							return;
3886					}
3887				}
3888			} else {
3889				_.init( options, element ).then( ready );
3890			}
3891		});
3892	};
3893
3894	/**
3895	 * @summary If supplied this method is executed after each template is initialized.
3896	 * @callback external:"jQuery.fn"~readyCallback
3897	 * @param {FooGallery.Template} template - The template that was initialized.
3898	 * @example {@caption The below shows an example of supplying this callback to the `.foogallery()` method.}
3899	 * jQuery(".foogallery").foogallery({
3900	 * 	// Options here
3901	 * }, function(template){
3902	 * 	// Called after each template is initialized on the matched elements
3903	 * });
3904	 */
3905	/**
3906	 * @summary Checks if the supplied image src is cached by the browser.
3907	 * @param {string} src - The image src to check.
3908	 * @returns {boolean}
3909	 */
3910	_.isCached = function(src){
3911		var img = new Image();
3912		img.src = src;
3913		var complete = img.complete;
3914		img.src = "";
3915		img = null;
3916		return complete;
3917	};
3918
3919	/**
3920	 * @summary A string array of supported EXIF properties.
3921	 * @memberof FooGallery
3922	 * @name supportedExifProperties
3923	 * @type {string[]}
3924	 */
3925	_.supportedExifProperties = ["camera","aperture","created_timestamp","shutter_speed","focal_length","iso","orientation"];
3926
3927	/**
3928	 * @memberof FooGallery.utils.is
3929	 * @function exif
3930	 * @param {*} value - The value to check.
3931	 * @returns {boolean} `true` if the `value` contains any supported and valid EXIF properties.
3932	 */
3933	_is.exif = function(value){
3934		if (_is.object(value)){
3935			var keys = Object.keys(value);
3936			return keys.length > 0 && keys.some(function(key){
3937				return _.supportedExifProperties.indexOf(key) !== -1 && !_is.empty(value[key]);
3938			});
3939		}
3940		return false;
3941	};
3942
3943	/**
3944	 * @summary Trims the value if it exceeds the specified length and appends the suffix.
3945	 * @memberof FooGallery.utils.str.
3946	 * @function trimTo
3947	 * @param {string} value - The value to trim if required.
3948	 * @param {number} length - The length to trim the string to.
3949	 * @param {string} [suffix="&hellip;"] - The suffix to append to a trimmed value.
3950	 * @returns {string|null}
3951	 */
3952	_str.trimTo = function(value, length, suffix){
3953		if (_is.string(value) && _is.number(length) && length > 0 && value.length > length) {
3954			return value.substr(0, length) + (_is.string(suffix) ? suffix : "&hellip;");
3955		}
3956		return value;
3957	};
3958
3959	/**
3960	 * @typedef {Object} ResizeObserverSize
3961	 * @property {number} inlineSize
3962	 * @property {number} blockSize
3963	 * @property {number} width
3964	 * @property {number} height
3965	 */
3966	/**
3967	 * @typedef {Object} ResizeObserverEntry
3968	 * @property {ResizeObserverSize|Array<ResizeObserverSize>|undefined} contentBoxSize
3969	 * @property {DOMRect} contentRect
3970	 */
3971	/**
3972	 * @summary Gets the width and height from the ResizeObserverEntry
3973	 * @memberof FooGallery.utils.
3974	 * @function getResizeObserverSize
3975	 * @param {ResizeObserverEntry} entry - The entry to retrieve the size from.
3976	 * @returns {{width: Number,height: Number}}
3977	 */
3978	_utils.getResizeObserverSize = function(entry){
3979		var width, height;
3980		if(entry.contentBoxSize) {
3981			// Checking for chrome as using a non-standard array
3982			if (entry.contentBoxSize[0]) {
3983				width = entry.contentBoxSize[0].inlineSize;
3984				height = entry.contentBoxSize[0].blockSize;
3985			} else {
3986				width = entry.contentBoxSize.inlineSize;
3987				height = entry.contentBoxSize.blockSize;
3988			}
3989		} else {
3990			width = entry.contentRect.width;
3991			height = entry.contentRect.height;
3992		}
3993		return {
3994			width: width,
3995			height: height
3996		};
3997	};
3998
3999	/**
4000	 * @summary Whether or not the current browser supports "webp" images.
4001	 * @memberof FooGallery.
4002	 * @name supportsWebP
4003	 * @type {boolean}
4004	 * @default false
4005	 */
4006	_.supportsWebP = false;
4007
4008	var webp = new Image();
4009	webp.onload = function(){
4010		_.supportsWebP = 0 < webp.width && 0 < webp.height;
4011	};
4012	webp.onerror=function(){
4013		_.supportsWebP = false;
4014	};
4015	webp.src = 'data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==';
4016
4017	/**
4018	 * @summary Simple test to see if the browser supports the <picture> element.
4019	 * @memberof FooGallery.
4020	 * @name supportsPicture
4021	 * @type {boolean}
4022	 */
4023	_.supportsPicture = !!window.HTMLPictureElement;
4024
4025	/**
4026	 * Utility class to make working with anonymous event listeners a bit simpler.
4027	 * @memberof FooGallery.utils.
4028	 * @class DOMEventListeners
4029	 * @augments FooGallery.utils.Class
4030	 * @borrows FooGallery.utils.Class.extend as extend
4031	 * @borrows FooGallery.utils.Class.override as override
4032	 */
4033	_utils.DOMEventListeners = _utils.Class.extend( /** @lends FooGallery.utils.DOMEventListeners.prototype */ {
4034		/**
4035		 * @ignore
4036		 * @constructs
4037		 **/
4038		construct: function(){
4039			/**
4040			 * A simple object containing the event listener and options.
4041			 * @typedef {Object} EventEntry
4042			 * @property {EventListener} listener
4043			 * @property {EventListenerOptions|boolean} [options]
4044			 */
4045			/**
4046			 * The map object containing all listeners.
4047			 * @type {Map<EventTarget, Map<string, EventEntry>>}
4048			 */
4049			this.eventTargets = new Map();
4050		},
4051		/**
4052		 * Add an event listener to the eventTarget.
4053		 * @param {EventTarget} eventTarget
4054		 * @param {string} type
4055		 * @param {EventListener} listener
4056		 * @param {AddEventListenerOptions|boolean} [options]
4057		 * @returns {boolean} False if a listener already exists for the element.
4058		 */
4059		add: function( eventTarget, type, listener, options ){
4060			eventTarget.addEventListener( type, listener, options );
4061			let listeners = this.eventTargets.get( eventTarget );
4062			if ( !listeners ){
4063				listeners = new Map();
4064				this.eventTargets.set( eventTarget, listeners );
4065			}
4066			let entry = listeners.get( type );
4067			if ( !entry ){
4068				listeners.set( type, { listener: listener, options: options } );
4069				return true;
4070			}
4071			return false;
4072		},
4073		/**
4074		 * Remove an event listener from the eventTarget.
4075		 * @param {EventTarget} eventTarget
4076		 * @param {string} type
4077		 */
4078		remove: function( eventTarget, type ){
4079			let listeners = this.eventTargets.get( eventTarget );
4080			if ( !listeners ) return;
4081			let entry = listeners.get( type );
4082			if ( !entry ) return;
4083			eventTarget.removeEventListener( type, entry.listener, entry.options );
4084			listeners.delete( type );
4085			if ( listeners.size === 0 ) this.eventTargets.delete( eventTarget );
4086		},
4087		/**
4088		 * Removes all event listeners from all eventTargets.
4089		 */
4090		clear: function(){
4091			this.eventTargets.forEach( function( listeners, eventTarget ){
4092				listeners.forEach( function( entry, type ){
4093					eventTarget.removeEventListener( type, entry.listener, entry.options );
4094				} );
4095			} );
4096			this.eventTargets.clear();
4097		}
4098	} );
4099
4100	/**
4101	 * Utility class to help with managing timeouts.
4102	 * @memberof FooGallery.utils.
4103	 * @class Timeouts
4104	 * @augments FooGallery.utils.Class
4105	 * @borrows FooGallery.utils.Class.extend as extend
4106	 * @borrows FooGallery.utils.Class.override as override
4107	 */
4108	_utils.Timeouts = _utils.Class.extend( /** @lends FooGallery.utils.Timeouts.prototype */ {
4109		/**
4110		 * @ignore
4111		 * @constructs
4112		 */
4113		construct: function(){
4114			const self = this;
4115			/**
4116			 * @typedef {Object} Timeout
4117			 * @property {number} id
4118			 * @property {number} delay
4119			 * @property {function} fn
4120			 */
4121			/**
4122			 * @type {Map<string, Timeout>}
4123			 * @private
4124			 */
4125			self.instances = new Map();
4126		},
4127		/**
4128		 * Returns a boolean indicating whether a timeout with the specified key exists or not.
4129		 * @param {string} key
4130		 * @returns {boolean}
4131		 */
4132		has: function( key ){
4133			return this.instances.has( key );
4134		},
4135		/**
4136		 * Returns the specified timeout if it exists.
4137		 * @param {string} key
4138		 * @returns {Timeout}
4139		 */
4140		get: function( key ){
4141			return this.instances.get( key );
4142		},
4143		/**
4144		 * Adds or updates a specified timeout.
4145		 * @param {string} key
4146		 * @param {function} callback
4147		 * @param {number} delay
4148		 * @returns {FooGallery.utils.Timeouts}
4149		 */
4150		set: function( key, callback, delay ){
4151			const self = this;
4152			self.delete( key );
4153			const timeout = {
4154				id: setTimeout( function(){
4155					self.instances.delete( key );
4156					callback.call( self );
4157				}, delay ),
4158				delay: delay,
4159				fn: callback
4160			};
4161			this.instances.set( key, timeout );
4162			return self;
4163		},
4164		/**
4165		 * Removes the specified timeout if it exists.
4166		 * @param {string} key
4167		 * @returns {boolean}
4168		 */
4169		delete: function( key ){
4170			const self = this;
4171			if ( self.instances.has( key ) ){
4172				const timeout = self.instances.get( key );
4173				clearTimeout( timeout.id );
4174				return self.instances.delete( key );
4175			}
4176			return false;
4177		},
4178		/**
4179		 * Removes all timeouts.
4180		 */
4181		clear: function(){
4182			const self = this;
4183			self.instances.forEach( function( timeout ){
4184				clearTimeout( timeout.id );
4185			} );
4186			self.instances.clear();
4187		}
4188	} );
4189
4190    /**
4191     * Checks if the supplied URL is cross-origin.
4192     * @param {string} url
4193     * @returns {boolean} True if the URL is cross-origin, otherwise False.
4194     */
4195	_.isCrossOrigin = function(url) {
4196		const parsed = _utils.url.parts(url);
4197		if ( parsed !== null ) {
4198			return parsed.origin !== window.location.origin;
4199		}
4200		return false;
4201	};
4202
4203    /**
4204     * Generates a UUID (version 4) according to RFC 4122. In modern browsers,
4205     * this method uses the `crypto.randomUUID()` method, which is more secure.
4206     * In older browsers, a fallback method is used.
4207     *
4208     * @returns {string} A UUID (version 4) according to RFC 4122.
4209     * @see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID
4210     * @see https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)
4211     */
4212    _.generateGUID = () => {
4213        // Check if the crypto object and randomUUID method are available
4214        if ( window.crypto && crypto.randomUUID ) {
4215            return crypto.randomUUID();
4216        } else {
4217            // Fallback method for non-secure contexts
4218            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( /[xy]/g, function( c ) {
4219                var r = Math.random() * 16 | 0,
4220                    v = c === 'x' ? r : ( r & 0x3 | 0x8 );
4221                return v.toString( 16 );
4222            } );
4223        }
4224    };
4225
4226	const _domParser = new DOMParser();
4227
4228	/**
4229	 * Uses the native DOMParser to decode HTMLEntities in a string.
4230	 * @param str
4231	 * @returns {string}
4232	 */
4233	_.decodeHTMLEntities = ( str ) => {
4234		let result = '';
4235		try {
4236			result = _domParser.parseFromString( str, "text/html" ).documentElement.textContent;
4237		} catch ( err ) {
4238			console.error( `Error decoding HTMLEntities in string "${ str }".`, err );
4239		}
4240		return result;
4241	};
4242
4243	/**
4244	 * Detects Facebook and Messenger in-app browsers.
4245	 * @param {?string} [userAgent=null]
4246	 * @returns {boolean}
4247	 */
4248	_.isFacebookMessengerBrowser = function( userAgent = null ){
4249		const ua = _is.string( userAgent ) ? userAgent : window.navigator.userAgent || "";
4250		if ( /facebookexternalhit|facebot/i.test( ua ) ) {
4251			return false;
4252		}
4253		return /\bFB_IAB\/(?:FB4A|MESSENGER|Orca-Android)\b|\bFBAN\/(?:FBIOS|FB4A|MessengerForiOS|MessengerLite|Messenger)\b|\bFBAV\//i.test( ua );
4254	};
4255
4256	/**
4257	 * More reliable way to trigger the download/Save As dialog for images than simply relying on a[download].
4258	 * @param {string} url
4259	 * @param {?string} [name=null]
4260	 * @returns {Promise<void>}
4261	 */
4262	_.downloadImage = function( url, name = null ){
4263		if ( _is.string( url ) && !_is.empty( url ) && _.isFacebookMessengerBrowser() ) {
4264			window.location.assign( url );
4265			return Promise.resolve();
4266		}
4267
4268		let downloadName = name;
4269		if ( !_is.string( downloadName ) || _is.empty( downloadName ) ) {
4270			downloadName = 'image';
4271			const parsed = _utils.url.parts( url );
4272			if ( parsed !== null && _is.string( parsed.pathname ) ) {
4273				const filename = parsed.pathname.replace( /\/+$/, '' ).split( '/' ).pop();
4274				if ( _is.string( filename ) && !_is.empty( filename ) ) {
4275					try {
4276						downloadName = decodeURIComponent( filename );
4277					} catch ( err ) {
4278						downloadName = filename;
4279					}
4280				}
4281			}
4282		}
4283		return fetch(url)
4284			.then(function (res) {
4285				if (!res.ok) {
4286					throw new Error(`Failed to fetch image: ${res.status} ${res.statusText}`);
4287				}
4288				return res.blob();
4289			})
4290			.then(function (blob) {
4291				const objectUrl = window.URL.createObjectURL(blob);
4292				try {
4293					const a = document.createElement('a');
4294					a.href = objectUrl;
4295					a.download = downloadName;
4296					document.body.appendChild(a);
4297					a.click();
4298					a.remove();
4299				} finally {
4300					// Always runs if objectUrl was created
4301					window.URL.revokeObjectURL(objectUrl);
4302				}
4303			});
4304	};
4305
4306})(
4307	FooGallery.$,
4308	FooGallery,
4309	FooGallery.utils,
4310	FooGallery.utils.is,
4311	FooGallery.utils.fn,
4312	FooGallery.utils.str
4313);
4314
4315( function( _ ) {
4316    const GlobalAttributes = new globalThis.Set( [ "accesskey", "autocapitalize", "autofocus", "class", "contenteditable", "data-", "dir", "draggable", "enterkeyhint", "exportparts", "hidden", "id", "inert", "inputmode", "is", "itemid", "itemprop", "itemref", "itemscope", "itemtype", "lang", "nonce", "part", "popover", "role", "slot", "spellcheck", "style", "tabindex", "title", "translate" ] );
4317    const ElementsAttributesMap = new globalThis.Map( [ [ "html", [ "xmlns" ] ], [ "base", [ "href", "target" ] ], [ "head", [] ], [ "link", [ "as", "crossorigin", "disabled", "fetchpriority", "href", "hreflang", "imagesizes", "imagesrcset", "integrity", "media", "referrerpolicy", "rel", "sizes", "type" ] ], [ "meta", [ "charset", "content", "http-equiv", "name" ] ], [ "style", [ "media" ] ], [ "title", [] ], [ "body", [ "onafterprint", "onbeforeprint", "onbeforeunload", "onblur", "onerror", "onfocus", "onhashchange", "onlanguagechange", "onload", "onmessage", "onoffline", "ononline", "onpopstate", "onresize", "onstorage", "onunload" ] ], [ "address", [] ], [ "article", [] ], [ "aside", [] ], [ "footer", [] ], [ "header", [] ], [ "h1", [] ], [ "h2", [] ], [ "h3", [] ], [ "h4", [] ], [ "h5", [] ], [ "h6", [] ], [ "hgroup", [] ], [ "main", [] ], [ "nav", [] ], [ "section", [] ], [ "search", [] ], [ "blockquote", [ "cite" ] ], [ "dd", [] ], [ "div", [] ], [ "dl", [] ], [ "dt", [] ], [ "figcaption", [] ], [ "figure", [] ], [ "hr", [] ], [ "li", [ "value" ] ], [ "menu", [] ], [ "ol", [ "reversed", "start", "type" ] ], [ "p", [] ], [ "pre", [] ], [ "ul", [] ], [ "a", [ "download", "href", "hreflang", "ping", "referrerpolicy", "rel", "target", "type" ] ], [ "abbr", [] ], [ "b", [] ], [ "bdi", [] ], [ "bdo", [] ], [ "br", [] ], [ "cite", [] ], [ "code", [] ], [ "data", [ "value" ] ], [ "dfn", [] ], [ "em", [] ], [ "i", [] ], [ "kbd", [] ], [ "mark", [] ], [ "q", [ "cite" ] ], [ "rp", [] ], [ "rt", [] ], [ "ruby", [] ], [ "s", [] ], [ "samp", [] ], [ "small", [] ], [ "span", [] ], [ "strong", [] ], [ "sub", [] ], [ "sup", [] ], [ "time", [ "datetime" ] ], [ "u", [] ], [ "var", [] ], [ "wbr", [] ], [ "area", [ "alt", "co
4317ords", "download", "href", "ping", "referrerpolicy", "rel", "shape", "target" ] ], [ "audio", [ "autoplay", "controls", "controlslist", "crossorigin", "disableremoteplayback", "loop", "muted", "preload", "src" ] ], [ "img", [ "alt", "crossorigin", "decoding", "elementtiming", "fetchpriority", "height", "ismap", "loading", "referrerpolicy", "sizes", "src", "srcset", "usemap", "width" ] ], [ "map", [ "name" ] ], [ "track", [ "default", "kind", "label", "src", "srclang" ] ], [ "video", [ "autoplay", "controls", "controlslist", "crossorigin", "disablepictureinpicture", "disableremoteplayback", "height", "loop", "muted", "playsinline", "poster", "preload", "src", "width" ] ], [ "embed", [ "height", "src", "type", "width" ] ], [ "iframe", [ "allow", "allowfullscreen", "height", "loading", "name", "referrerpolicy", "sandbox", "src", "width" ] ], [ "object", [ "data", "form", "height", "name", "type", "width" ] ], [ "picture", [] ], [ "portal", [ "referrerpolicy", "src" ] ], [ "source", [ "height", "media", "sizes", "src", "srcset", "type", "width" ] ], [ "svg", [ "height", "preserveaspectratio", "viewbox", "width", "x", "y" ] ], [ "canvas", [ "height", "width" ] ], [ "noscript", [] ], [ "script", [ "async", "crossorigin", "defer", "fetchpriority", "integrity", "nomodule", "referrerpolicy", "src", "type" ] ], [ "del", [ "cite", "datetime" ] ], [ "ins", [ "cite", "datetime" ] ], [ "caption", [] ], [ "col", [ "span" ] ], [ "colgroup", [ "span" ] ], [ "table", [] ], [ "tbody", [] ], [ "td", [ "colspan", "headers", "rowspan" ] ], [ "tfoot", [] ], [ "th", [ "abbr", "colspan", "headers", "rowspan", "scope" ] ], [ "thead", [] ], [ "tr", [] ], [ "button", [ "disabled", "form", "formaction", "formenctype", "formmethod", "formnovalidate", "formtarget", "name", "popovertarget", "popovertargetaction", "type", "value" ] ], [ "datalist", [] ], [ "fieldset", [ "disabled", "form", "name" ] ], [ "form", [ "accept-charset", "autocomplete", "name", "rel" ] ], [ "input", [] ], [ "label", [ "for" ] ], [ "legend", [] ], [ "meter", [ "form", "high", "low", "max", "min", "optimum", "value" ] ], [ "optgroup", [ "disabled", "label" ] ], [ "option", [ "disabled", "label", "selected", "value" ] ], [ "output", [ "for", "form", "name" ] ], [ "progress", [ "max", "value" ] ], [ "select", [ "autocomplete", "disabled", "form", "multiple", "name", "required", "size" ] ], [ "textarea", [ "autocomplete", "cols", "dirname", "disabled", "form", "maxlength", "minlength", "name", "placeholder", "readonly", "required", "rows", "wrap" ] ], [ "details", [ "name", "open" ] ], [ "dialog", [ "open" ] ], [ "summary", [] ], [ "slot", [ "name" ] ], [ "template", [ "shadowrootclonable", "shadowrootdelegatesfocus", "shadowrootmode" ] ] ] );
4318    const UrlAttributes = new globalThis.Set( [ 'code', 'codebase', 'src', 'href', 'formaction', 'ping', 'cite', 'action', 'background', 'poster', 'profile', 'manifest', 'data' ] );
4319
4320    // always remove these elements
4321    [ 'script', 'embed', 'object', 'iframe', 'portal', 'base', 'link', 'meta', 'style', 'noscript', 'template', 'svg' ].forEach( x => ElementsAttributesMap.delete( x ) );
4322
4323    const getNodeName = node => ( node instanceof globalThis.Node ? node.nodeName : typeof node === 'string' ? node : '' ).toLowerCase();
4324
4325    const isElementAttribute = attr => {
4326        if ( attr instanceof globalThis.Attr ) {
4327            const ownerElement = getNodeName( attr.ownerElement );
4328            if ( ElementsAttributesMap.has( ownerElement ) ) {
4329                return ElementsAttributesMap.get( ownerElement ).includes( attr.name );
4330            }
4331        }
4332        return false;
4333    }
4334
4335    const isSafeUrlRegex = /^(?!javascript|vbscript|livescript|mocha)(?:[a-z0-9+.-]+:[^<>]*$|[^&:\/?#]*(?:[\/?#]|$))/i;
4336    const isSafeUrlAttribute = string => isSafeUrlRegex.test( string );
4337
4338    const dangerousStyleRegex = /@import|expression|behaviou?r|binding|(?:javascript|vbscript|livescript|mocha):|[\x00-\x08\x0E-\x1F\x7F-\uFFFF]|\/\*.*?\*\/|<--.*?-->/i;
4339    const isSafeStyleAttribute = value => !dangerousStyleRegex.test( value.replace( /\s+/g, ' ' ) )
4340
4341
4342    const isPossibleAttributeRegex = /^[a-z](?:[\x2D.0-9_a-z\xB7\xC0-\xD6\xD8-\xF6\xF8-\u037D\u037F-\u1FFF\u200C\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]|[\uD800-\uDB7F][\uDC00-\uDFFF])*$/;
4343    const isPossibleAttribute = attr => isPossibleAttributeRegex.test( getNodeName( attr ) );
4344
4345    // https://html.spec.whatwg.org/multipage/scripting.html#valid-custom-element-name
4346    const isPossibleCustomElementRegex = /^[a-z](?:[.0-9_a-z\xB7\xC0-\xD6\xD8-\xF6\xF8-\u037D\u037F-\u1FFF\u200C\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]|[\uD800-\uDB7F][\uDC00-\uDFFF])*-(?:[\x2D.0-9_a-z\xB7\xC0-\xD6\xD8-\xF6\xF8-\u037D\u037F-\u1FFF\u200C\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]|[\uD800-\uDB7F][\uDC00-\uDFFF])*$/;
4347    const isPossibleCustomElement = element => isPossibleCustomElementRegex.test( getNodeName( element ) );
4348
4349    const isPossibleCustomAttribute = attr => isPossibleCustomElement( attr?.ownerElement ) && isPossibleAttribute( attr );
4350
4351    const isSafeAttribute = attr => {
4352        const name = getNodeName( attr );
4353        if ( !name.startsWith( 'on' ) ) {
4354            if ( name.startsWith( 'aria-' ) || name.startsWith( 'data-' ) ) {
4355                return true;
4356            }
4357            if ( GlobalAttributes.has( name ) || isElementAttribute( attr ) ) {
4358                if ( UrlAttributes.has( name ) ) {
4359                    return isSafeUrlAttribute( attr.value );
4360                }
4361                if ( name === 'style' ) {
4362                    return isSafeStyleAttribute( attr.value );
4363                }
4364                return true;
4365            }
4366            return isPossibleCustomAttribute( attr );
4367        }
4368        return false;
4369    };
4370
4371    const isSafeElement = element => {
4372        if ( element instanceof globalThis.Element ) {
4373            const name = getNodeName( element );
4374            if ( element.namespaceURI === 'http://www.w3.org/1999/xhtml' && ElementsAttributesMap.has( name ) ) {
4375                for ( const attr of element.attributes ) {
4376                    if ( !isSafeAttribute( attr ) ) {
4377                        return false;
4378                    }
4379                }
4380                if ( name === 'style' ) {
4381                    return isSafeStyleAttribute( element.textContent );
4382                }
4383                return true;
4384            }
4385        }
4386        return false;
4387    };
4388
4389    const cloneNode = ( node, deep = false ) => {
4390        if ( node instanceof globalThis.Element ) {
4391            if ( isSafeElement( node ) ) {
4392                const element = node.cloneNode( false );
4393                if ( deep && node.hasChildNodes() ) {
4394                    element.append( ...cloneNodes( node.childNodes, deep ) );
4395                }
4396                return element;
4397            }
4398            throw new TypeError( 'UNSAFE_NODE' );
4399        }
4400        if ( node instanceof globalThis.Text ) {
4401            return node.cloneNode();
4402        }
4403        return null;
4404    };
4405
4406    const cloneNodes = ( nodes, deep = false ) => {
4407        const result = [];
4408        for ( const node of nodes ) {
4409            const cloned = cloneNode( node, deep );
4410            if ( cloned instanceof globalThis.Node ) {
4411                result.push( cloned );
4412            }
4413        }
4414        return result;
4415    };
4416
4417    let parserInstance;
4418    const parser = () => {
4419        if ( parserInstance instanceof globalThis.DOMParser ) return parserInstance;
4420        return parserInstance = new globalThis.DOMParser();
4421    };
4422
4423    const toHTML = nodes => nodes.map( node => {
4424        if ( node.nodeType === 1 ) return node.outerHTML;
4425        // Preserve text across the caller's subsequent innerHTML parse.
4426        if ( node.nodeType === 3 ) return node.nodeValue.replace( /&/g, '&amp;' ).replace( /</g, '&lt;' ).replace( />/g, '&gt;' );
4427        return '';
4428    } ).join( '' );
4429
4430    _.safeParse = html => {
4431        if ( typeof html === 'string' ) {
4432            try {
4433                const doc = parser().parseFromString( html, 'text/html' );
4434                if ( doc.body.hasChildNodes() ) {
4435                    const nodes = cloneNodes( doc.body.childNodes, true );
4436                    return toHTML( nodes );
4437                }
4438            } catch ( e ) {
4439                if ( e.message !== 'UNSAFE_NODE' ) {
4440                    console.error( 'FooGallery.safeParse: Unexpected Error', e );
4441                }
4442            }
4443        }
4444        return '';
4445    };
4446} )( window.FooGallery );
4447(function($, _, _utils, _is, _obj){
4448
4449    _.Icons = _utils.Class.extend({
4450        construct: function(){
4451            this.className = "fg-icon";
4452            this.registered = {
4453                "default": {
4454                    "close": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M13.957 3.457l-1.414-1.414-4.543 4.543-4.543-4.543-1.414 1.414 4.543 4.543-4.543 4.543 1.414 1.414 4.543-4.543 4.543 4.543 1.414-1.414-4.543-4.543z"></path></svg>',
4455                    "arrow-left": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M10.5 16l1.5-1.5-6.5-6.5 6.5-6.5-1.5-1.5-8 8 8 8z"></path></svg>',
4456                    "arrow-right": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewB
4456ox="0 0 16 16"><path d="M5.5 0l-1.5 1.5 6.5 6.5-6.5 6.5 1.5 1.5 8-8-8-8z"></path></svg>',
4457                    "arrow-down": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M3.5 6l4.5 4.5 4.5-4.5h-9z"></path></svg>',
4458                    "maximize": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M2 2v4h-2v-5c0-0.552 0.448-1 1-1h14c0.552 0 1 0.448 1 1v14c0 0.552-0.448 1-1 1h-14c-0.552 0-1-0.448-1-1v-9h9c0.552 0 1 0.448 1 1v7h4v-12h-12z"/></svg>',
4459                    "expand": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M2 5h-2v-4c0-0.552 0.448-1 1-1h4v2h-3v3z"></path><path d="M16 5h-2v-3h-3v-2h4c0.552 0 1 0.448 1 1v4z"></path><path d="M15 16h-4v-2h3v-3h2v4c0 0.552-0.448 1-1 1z"></path><path d="M5 16h-4c-0.552 0-1-0.448-1-1v-4h2v3h3v2z"></path></svg>',
4460                    "shrink": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M3 0h2v4c0 0.552-0.448 1-1 1h-4v-2h3v-3z"></path><path d="M11 0h2v3h3v2h-4c-0.552 0-1-0.448-1-1v-4z"></path><path d="M12 11h4v2h-3v3h-2v-4c0-0.552 0.448-1 1-1z"></path><path d="M0 11h4c0.552 0 1 0.448 1 1v4h-2v-3h-3v-2z"></path></svg>',
4461                    "info": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M7 4.75c0-0.412 0.338-0.75 0.75-0.75h0.5c0.412 0 0.75 0.338 0.75 0.75v0.5c0 0.412-0.338 0.75-0.75 0.75h-0.5c-0.412 0-0.75-0.338-0.75-0.75v-0.5z"></path><path d="M10 12h-4v-1h1v-3h-1v-1h3v4h1z"></path><path d="M8 0c-4.418 0-8 3.582-8 8s3.582 8 8 8 8-3.582 8-8-3.582-8-8-8zM8 14.5c-3.59 0-6.5-2.91-6.5-6.5s2.91-6.5 6.5-6.5 6.5 2.91 6.5 6.5-2.91 6.5-6.5 6.5z"></path></svg>',
4462                    "comment": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M3 4h10v1h-10zM3 6h8v1h-8zM3 8h4v1h-4zM14.5 1h-13c-0.825 0-1.5 0.675-1.5 1.5v8c0 0.825 0.675 1.5 1.5 1.5h2.5v4l4.8-4h5.7c0.825 0 1.5-0.675 1.5-1.5v-8c0-0.825-0.675-1.5-1.5-1.5zM14 10h-5.924l-3.076 2.73v-2.73h-3v-7h12v7z"></path></svg>',
4463                    "thumbs": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M3 3v10h-2v-11c0-0.552 0.448-1 1-1h12c0.552 0 1 0.448 1 1v12c0 0.552-0.448 1-1 1h-12c-0.552 0-1-0.448-1-1v-1h4v-2h-2v-2h2v-2h-2v-2h2v-2h2v10h6v-10h-10z"></path></svg>',
4464                    "cart": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M13.238 9c0.55 0 1.124-0.433 1.275-0.962l1.451-5.077c0.151-0.529-0.175-0.962-0.725-0.962h-10.238c0-1.105-0.895-2-2-2h-3v2h3v8.5c0 0.828 0.672 1.5 1.5 1.5h9.5c0.552 0 1-0.448 1-1s-0.448-1-1-1h-9v-1h8.238zM5 4h9.044l-0.857 3h-8.187v-3z"></path><path d="M6 14.5c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5c0-0.828 0.672-1.5 1.5-1.5s1.5 0.672 1.5 1.5z"></path><path d="M15 14.5c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5c0-0.828 0.672-1.5 1.5-1.5s1.5 0.672 1.5 1.5z"></path></svg>',
4465                    "download": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M2.5 7l5.5 5.5 5.5-5.5h-3.5v-6h-4v6z"></path><path d="M14 8v6h-12v-6h-2v6.5c0 0.825 0.675 1.5 1.5 1.5h13c0.825 0 1.5-0.675 1.5-1.5v-6.5h-2z"></path></svg>',
4466                    "circle-close": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 0c-4.418 0-8 3.582-8 8s3.582 8 8 8 8-3.582 8-8-3.582-8-8-8zM8 14.5c-3.59 0-6.5-2.91-6.5-6.5s2.91-6.5 6.5-6.5 6.5 2.91 6.5 6.5-2.91 6.5-6.5 6.5z"></path><path d="M10.5 4l-2.5 2.5-2.5-2.5-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 2.5-2.5 2.5 2.5 1.5-1.5-2.5-2.5 2.5-2.5z"></path></svg>',
4467                    "circle-plus": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 0c-4.418 0-8 3.582-8 8s3.582 8 8 8 8-3.582 8-8-3.582-8-8-8zM8 14c-3.314 0-6-2.686-6-6s2.686-6 6-6c3.314 0 6 2.686 6 6s-2.686 6-6 6zM12 9h-3v3h-2v-3h-3v-2h3v-3h2v3h3z"></path></svg>',
4468                    "circle-minus": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewB
4468ox="0 0 16 16"><path d="M8 0c-4.418 0-8 3.582-8 8s3.582 8 8 8 8-3.582 8-8-3.582-8-8-8zM8 14c-3.314 0-6-2.686-6-6s2.686-6 6-6c3.314 0 6 2.686 6 6s-2.686 6-6 6zM4 7h8v2h-8z"></path></svg>',
4469                    "heart": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M16 5.596c0 1.349-0.582 2.563-1.508 3.404h0.008l-5 5c-0.5 0.5-1 1-1.5 1s-1-0.5-1.5-1l-4.992-5c-0.926-0.841-1.508-2.054-1.508-3.404 0-2.538 2.058-4.596 4.596-4.596 1.349 0 2.563 0.582 3.404 1.508 0.841-0.926 2.054-1.508 3.404-1.508 2.538 0 4.596 2.058 4.596 4.596z"></path></svg>',
4470                    "heart-outline": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M16 5.596c0-2.538-2.058-4.596-4.596-4.596-1.349 0-2.563 0.582-3.404 1.508-0.841-0.926-2.054-1.508-3.404-1.508-2.538 0-4.596 2.058-4.596 4.596 0 1.349 0.582 2.563 1.508 3.404h-0.008l5 5c0.5 0.5 1 1 1.5 1s1-0.5 1.5-1l4.992-5c0.926-0.841 1.508-2.054 1.508-3.404zM13.148 7.519l-5.062 5.067c-0.026 0.026-0.055 0.055-0.086 0.086-0.031-0.030-0.060-0.059-0.086-0.086l-5.062-5.066c-0.549-0.499-0.852-1.182-0.852-1.923 0-1.432 1.165-2.596 2.596-2.596 0.741 0 1.424 0.303 1.923 0.852l1.481 1.631 1.481-1.631c0.499-0.55 1.182-0.852 1.923-0.852 1.432 0 2.596 1.165 2.596 2.596 0 0.741-0.303 1.424-0.852 1.923z"></path></svg>',
4471                    "auto-progress": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path class="[ICON_CLASS]-idle" d="M11.39 8c2.152-1.365 3.61-3.988 3.61-7 0-0.339-0.019-0.672-0.054-1h-13.891c-0.036 0.328-0.054 0.661-0.054 1 0 3.012 1.457 5.635 3.609 7-2.152 1.365-3.609 3.988-3.609 7 0 0.339 0.019 0.672 0.054 1h13.891c0.036-0.328 0.054-0.661 0.054-1 0-3.012-1.457-5.635-3.609-7zM2.5 15c0-2.921 1.253-5.397 3.5-6.214v-1.572c-2.247-0.817-3.5-3.294-3.5-6.214v0h11c0 2.921-1.253 5.397-3.5 6.214v1.572c2.247 0.817 3.5 3.294 3.5 6.214h-11zM9.462 10.462c-1.12-0.635-1.181-1.459-1.182-1.959v-1.004c0-0.5 0.059-1.327 1.184-1.963 0.602-0.349 1.122-0.88 1.516-1.537h-6.4c0.395 0.657 0.916 1.188 1.518 1.538 1.12 0.635 1.181 1.459 1.182 1.959v1.004c0 0.5-0.059 1.327-1.184 1.963-1.135 0.659-1.98 1.964-2.236 3.537h7.839c-0.256-1.574-1.102-2.879-2.238-3.538z"/><circle class="[ICON_CLASS]-circle" r="4" cx="8" cy="8"/><path class="[ICON_CLASS]-play" d="M3 2l10 6-10 6z"/><path class="[ICON_CLASS]-pause" d="M2 2h5v12h-5zM9 2h5v12h-5z"/></svg>',
4472                    "search": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M16 13.5l-4.695-4.695c0.444-0.837 0.695-1.792 0.695-2.805 0-3.314-2.686-6-6-6s-6 2.686-6 6 2.686 6 6 6c1.013 0 1.968-0.252 2.805-0.695l4.695 4.695 2.5-2.5zM2 6c0-2.209 1.791-4 4-4s4 1.791 4 4-1.791 4-4 4-4-1.791-4-4z"></path></svg>',
4473                    "spinner": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 16c-2.137 0-4.146-0.832-5.657-2.343s-2.343-3.52-2.343-5.657c0-1.513 0.425-2.986 1.228-4.261 0.781-1.239 1.885-2.24 3.193-2.895l0.672 1.341c-1.063 0.533-1.961 1.347-2.596 2.354-0.652 1.034-0.997 2.231-0.997 3.461 0 3.584 2.916 6.5 6.5 6.5s6.5-2.916 6.5-6.5c0-1.23-0.345-2.426-0.997-3.461-0.635-1.008-1.533-1.822-2.596-2.354l0.672-1.341c1.308 0.655 2.412 1.656 3.193 2.895 0.803 1.274 1.228 2.748 1.228 4.261 0 2.137-0.832 4.146-2.343 5.657s-3.52 2.343-5.657 2.343z"></path></svg>',
4474                    "exif-aperture": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M 8,0 C 3.5876443,0 0,3.5876443 0,8 c 0,4.412356 3.5876443,8 8,8 4.412356,0 8,-3.587644 8,-8 C 16,3.5876443 12.412356,0 8,0 Z m 0,1 c 3.871916,0 7,3.1280844 7,7 0,3.871916 -3.128084,7 -7,7 C 4.1280844,15 1,11.871916 1,8 1,4.1280844 4.1280844,1 8,1 Z M 7.53125,2.0214844 A 6,6 0 0 0 3.1835938,4.4335938 H 8.9257812 Z M 8.6132812,2.03125 C 9.5587451,3.6702105 10.504247,5.3091484 11.451172,6.9472656 L 12.863281,4.5 A 6,6 0 0 0 8.6132812,2.03125 Z M 2.5957031,5.4101562 A 6,6 0 0 0 2,8 6,6 0 0 0 2.5273438,10.439453 L 5.4296875,5.4101562 Z m 10.8261719,0.033203 -2.855469,4.9433598 h 2.935547 A 6,6 0 0 0 14,8 6,6 0 0 0 13.421875,5.4433592 Z M 4.5722656,8.8945312 3.0996094,11.449219 a 6,6 0 0 0 4.40625,2.527343 z m 2.5820313,2.4707028 1.4960937,2.591797 a 6,6 0 0 0 4.3144534,-2.591797 z"></path></svg>',
4475                    "exif-camera": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewB
4475ox="0 0 16 16"><path d="m 8.0000002,5 a 4,4 0 0 0 -4,4 4,4 0 0 0 4,4 A 4,4 0 0 0 12,9 4,4 0 0 0 8.0000002,5 Z m 0.019531,1.015625 a 3,2.9814477 0 0 1 2.9804688,3 l -1,-0.00586 a 2,2 0 0 0 0,-0.00976 2,2 0 0 0 -1.9863279,-2 z M 5.125,1 C 4.5,1 4,1.5 4,2.125 V 3.0000004 L 1.125,3 C 0.5,2.9999999 0,3.5 0,4.125 v 9.75 C 0,14.5 0.5,15 1.125,15 h 13.75 C 15.5,15 16,14.5 16,13.875 V 4.125 C 16,3.5 15.5,3 14.875,3 H 12 V 2.125 C 12,1.5 11.5,1 10.875,1 Z M 5.25,2.0000004 h 5.5 c 0.125,0 0.25,0.1249996 0.25,0.25 v 1.75 h 3.75 c 0.125,0 0.25,0.1249996 0.25,0.25 V 13.75 C 15,13.875 14.875,14 14.75,14 H 1.25 C 1.125,14 1,13.875 1,13.75 V 4.25 C 1,4.125 1.125,4 1.25,4 l 3.75,4e-7 v -1.75 c 0,-0.1250004 0.125,-0.25 0.25,-0.25 z"></path></svg>',
4476                    "exif-created-timestamp": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M 3,1 V 2 H 1 V 15 H 16 V 2 H 14 V 1 H 13 V 2 H 4 V 1 Z M 2,4 H 15 V 14 H 2 Z M 6,5 V 7 H 8 V 5 Z m 3,0 v 2 h 2 V 5 Z m 3,0 v 2 h 2 V 5 Z M 3,8 v 2 H 5 V 8 Z m 3,0 v 2 H 8 V 8 Z m 3,0 v 2 h 2 V 8 Z m 3,0 v 2 h 2 V 8 Z m -9,3 v 2 h 2 v -2 z m 3,0 v 2 h 2 v -2 z m 3,0 v 2 h 2 v -2 z"></path></svg>',
4477                    "exif-shutter-speed": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M 8,0 C 3.5876443,0 -2.9415707e-8,3.5876443 0,8 c 2.3532563e-7,4.412356 3.5876445,8 8,8 4.412356,0 8,-3.587644 8,-8 C 16,3.5876443 12.412356,0 8,0 Z m 0,1 c 3.871916,0 7,3.1280844 7,7 0,3.871915 -3.128085,7 -7,7 -3.8719154,0 -6.9999998,-3.128085 -7,-7 -3e-8,-3.8719156 3.1280844,-7 7,-7 z M 11.646484,3.6464844 8.6445312,6.6484375 A 1.5,1.5 0 0 0 8,6.5 1.5,1.5 0 0 0 6.5,8 1.5,1.5 0 0 0 8,9.5 1.5,1.5 0 0 0 9.5,8 1.5,1.5 0 0 0 9.3515625,7.3554688 L 12.353516,4.3535156 Z M 2,7.5 v 1 h 2 v -1 z M 7.5,12 v 2 h 1 V 12 Z M 12,7.5 v 1 h 2 v -1 z M 7.5,2 v 2 h 1 V 2 Z"></path></svg>',
4478                    "exif-focal-length": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="m 1,4.125 -1,0.25 v 7.25 l 1,0.25 z M 5,1 2,4 v 8 l 3,3 h 6.875 C 12.500003,15 13,14.5 13,13.875 V 2.125 C 13,1.4999986 12.5,1 11.875,1 9.576807,0.99914375 7.1414067,0.96597644 5,1 Z M 5.5,2 H 6 V 14 H 5.5 L 3,11.5 v -7 z M 7,2 h 4.75 C 11.875,2 12,2.1249997 12,2.25 v 11.5 c 0,0.125 -0.125,0.250622 -0.25,0.25 H 7 Z m 7,0 c 1,2.2 1.5,4.35 1.5,6 0,1.65 -0.5,3.8 -1.5,6"></path></svg>',
4479                    "exif-iso": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M 7.5,0 V 1.6015625 C 6.0969201,1.7146076 4.8392502,2.256185 3.828125,3.1210938 L 2.6035156,1.8964844 1.8964844,2.6035156 3.1210938,3.828125 C 2.256185,4.8392502 1.7146076,6.0969201 1.6015625,7.5 H 0 v 1 h 1.6015625 c 0.1130451,1.4030799 0.6546225,2.66075 1.5195313,3.671875 l -1.2246094,1.224609 0.7070312,0.707032 1.2246094,-1.22461 C 4.8392502,13.743815 6.0969201,14.285392 7.5,14.398438 V 16 h 1 v -1.601562 c 1.4030799,-0.113046 2.66075,-0.654623 3.671875,-1.519532 l 1.224609,1.22461 0.707032,-0.707032 -1.22461,-1.224609 C 13.743815,11.16075 14.285392,9.9030799 14.398438,8.5 H 16 v -1 H 14.398438 C 14.285392,6.0969201 13.743815,4.8392502 12.878906,3.828125 L 14.103516,2.6035156 13.396484,1.8964844 12.171875,3.1210938 C 11.16075,2.256185 9.9030799,1.7146076 8.5,1.6015625 V 0 Z M 8,2.5 c 3.043488,0 5.5,2.4565116 5.5,5.5 0,3.043488 -2.456512,5.5 -5.5,5.5 C 4.9565116,13.5 2.5,11.043488 2.5,8 2.5,4.9565116 4.9565116,2.5 8,2.5 Z"></path></svg>',
4480                    "exif-orientation": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M 1.25,0 C 0.625,0 0,0.625 0,1.25 V 5 H 1 V 3 h 8 v 2 h 1 V 1.25 C 10,0.625 9.375,0 8.75,0 Z m 0,1 h 7.5 C 8.875,1 9,1.125 9,1.25 V 2 H 1 V 1.25 C 1,1.125 1.125,1 1.25,1 Z m 0,5 C 0.625,6 0,6.625 0,7.25 v 7.5 C 0,15.375 0.625,16 1.25,16 h 13.5 C 15.375,16 16,15.375 16,14.75 V 7.25 C 16,6.625 15.375,6 14.75,6 Z m 0,1 H 2 v 3 H 1 V 7.25 C 1,7.125 1.125,7 1.25,7 Z M 3,7 h 10 v 8 H 3 Z m 11,0 h 0.75 C 14.875,7 15,7.125 15,7.25 v 7.5 C 15,14.875 14.875,15 14.75,15 H 14 Z M 1,12 h 1 v 3 H 1.25 C 1.125,15 1,14.875 1,14.75 Z"></path></svg>',
4481                    "social-share": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewB
4481ox="0 0 16 16"><path d="M13.5 11c-0.706 0-1.342 0.293-1.797 0.763l-6.734-3.367c0.021-0.129 0.032-0.261 0.032-0.396s-0.011-0.267-0.032-0.396l6.734-3.367c0.455 0.47 1.091 0.763 1.797 0.763 1.381 0 2.5-1.119 2.5-2.5s-1.119-2.5-2.5-2.5-2.5 1.119-2.5 2.5c0 0.135 0.011 0.267 0.031 0.396l-6.734 3.367c-0.455-0.47-1.091-0.763-1.797-0.763-1.381 0-2.5 1.119-2.5 2.5s1.119 2.5 2.5 2.5c0.706 0 1.343-0.293 1.797-0.763l6.734 3.367c-0.021 0.129-0.031 0.261-0.031 0.396 0 1.381 1.119 2.5 2.5 2.5s2.5-1.119 2.5-2.5c0-1.381-1.119-2.5-2.5-2.5z"></path></svg>',
4482                    "social-facebook": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M9.5 3h2.5v-3h-2.5c-1.93 0-3.5 1.57-3.5 3.5v1.5h-2v3h2v8h3v-8h2.5l0.5-3h-3v-1.5c0-0.271 0.229-0.5 0.5-0.5z"></path></svg>',
4483                    "social-linkedin": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M6 6h2.767v1.418h0.040c0.385-0.691 1.327-1.418 2.732-1.418 2.921 0 3.461 1.818 3.461 4.183v4.817h-2.885v-4.27c0-1.018-0.021-2.329-1.5-2.329-1.502 0-1.732 1.109-1.732 2.255v4.344h-2.883v-9z"></path><path d="M1 6h3v9h-3v-9z"></path><path d="M4 3.5c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5c0-0.828 0.672-1.5 1.5-1.5s1.5 0.672 1.5 1.5z"></path></svg>',
4484                    "social-pinterest": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 1.069c-3.828 0-6.931 3.103-6.931 6.931 0 2.938 1.828 5.444 4.406 6.453-0.059-0.547-0.116-1.391 0.025-1.988 0.125-0.541 0.813-3.444 0.813-3.444s-0.206-0.416-0.206-1.028c0-0.963 0.559-1.684 1.253-1.684 0.591 0 0.878 0.444 0.878 0.975 0 0.594-0.378 1.484-0.575 2.306-0.166 0.691 0.344 1.253 1.025 1.253 1.231 0 2.178-1.3 2.178-3.175 0-1.659-1.194-2.819-2.894-2.819-1.972 0-3.128 1.478-3.128 3.009 0 0.597 0.228 1.234 0.516 1.581 0.056 0.069 0.066 0.128 0.047 0.2-0.053 0.219-0.169 0.691-0.194 0.787-0.031 0.128-0.1 0.153-0.231 0.094-0.866-0.403-1.406-1.669-1.406-2.684 0-2.188 1.587-4.194 4.578-4.194 2.403 0 4.272 1.712 4.272 4.003 0 2.388-1.506 4.313-3.597 4.313-0.703 0-1.362-0.366-1.588-0.797 0 0-0.347 1.322-0.431 1.647-0.156 0.603-0.578 1.356-0.862 1.816 0.65 0.2 1.337 0.309 2.053 0.309 3.828 0 6.931-3.103 6.931-6.931 0-3.831-3.103-6.934-6.931-6.934z"></path></svg>',
4485                    "social-reddit": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M4 10c0-0.552 0.448-1 1-1s1 0.448 1 1c0 0.552-0.448 1-1 1s-1-0.448-1-1zM10 10c0-0.552 0.448-1 1-1s1 0.448 1 1c0 0.552-0.448 1-1 1s-1-0.448-1-1zM10.049 12.137c0.258-0.203 0.631-0.159 0.834 0.099s0.159 0.631-0.099 0.834c-0.717 0.565-1.81 0.93-2.783 0.93s-2.066-0.365-2.784-0.93c-0.258-0.203-0.302-0.576-0.099-0.834s0.576-0.302 0.834-0.099c0.413 0.325 1.23 0.675 2.049 0.675s1.636-0.35 2.049-0.675zM16 8c0-1.105-0.895-2-2-2-0.752 0-1.406 0.415-1.748 1.028-1.028-0.562-2.28-0.926-3.645-1.010l1.193-2.68 2.284 0.659c0.206 0.583 0.761 1.002 1.415 1.002 0.828 0 1.5-0.672 1.5-1.5s-0.672-1.5-1.5-1.5c-0.571 0-1.068 0.319-1.321 0.789l-2.545-0.735c-0.285-0.082-0.587 0.058-0.707 0.329l-1.621 3.641c-1.33 0.094-2.551 0.453-3.557 1.004-0.342-0.613-0.996-1.028-1.748-1.028-1.105 0-2 0.895-2 2 0 0.817 0.491 1.52 1.193 1.83-0.126 0.375-0.193 0.767-0.193 1.17 0 2.761 3.134 5 7 5s7-2.239 7-5c0-0.403-0.067-0.795-0.193-1.17 0.703-0.31 1.193-1.013 1.193-1.83zM13.5 2.938c0.311 0 0.563 0.252 0.563 0.563s-0.252 0.563-0.563 0.563-0.563-0.252-0.563-0.563 0.252-0.563 0.563-0.563zM1 8c0-0.551 0.449-1 1-1 0.399 0 0.743 0.234 0.904 0.573-0.523 0.396-0.956 0.854-1.276 1.355-0.368-0.148-0.628-0.508-0.628-0.928zM8 14.813c-3.21 0-5.813-1.707-5.813-3.813s2.602-3.813 5.813-3.813c3.21 0 5.813 1.707 5.813 3.813s-2.602 3.813-5.813 3.813zM14.372 8.928c-0.32-0.502-0.753-0.959-1.276-1.355 0.161-0.338 0.505-0.573 0.904-0.573 0.551 0 1 0.449 1 1 0 0.42-0.26 0.78-0.628 0.928z"></path></svg>',
4486                    "social-tumblr": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M9.001 7l-0 3.659c0 0.928-0.012 1.463 0.086 1.727 0.098 0.262 0.342 0.534 0.609 0.691 0.354 0.212 0.758 0.318 1.214 0.318 0.81 0 1.289-0.107 2.090-0.633v2.405c-0.683 0.321-1.279 0.509-1.833 0.639-0.555 0.129-1.154 0.194-1.798 0.194-0.732 0-1.163-0.092-1.725-0.276-0.562-0.185-1.042-0.45-1.438-0.79-0.398-0.343-0.672-0.706-0.826-1.091s-0.23-0.944-0.23-1.676v-5.611h-2.147v-2.266c0.628-0.204 1.331-0.497 1.778-0.877 0.449-0.382 0.809-0.839 1.080-1.374 0.272-0.534 0.459-1.214 0.561-2.039h2.579l-0 4h3.999v3h-3.999z"></path></svg>',
4487                    "social-twitter": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewB
4487ox="0 0 16 16"><path d="M12.163 1.5h2.205l-4.818 5.507 5.668 7.493h-4.438l-3.476-4.545-3.977 4.545h-2.207l5.153-5.89-5.437-7.11h4.551l3.142 4.154zM11.389 13.18h1.222l-7.888-10.429h-1.311z"></path></svg>',
4488                    "social-vk": '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><path d="M14.977 4.063c0.117 0.32-0.25 1.070-1.172 2.297-1.516 2.016-1.68 1.828-0.43 2.992 1.203 1.117 1.453 1.656 1.492 1.727 0 0 0.5 0.875-0.555 0.883l-2 0.031c-0.43 0.086-1-0.305-1-0.305-0.75-0.516-1.453-1.852-2-1.68 0 0-0.563 0.18-0.547 1.383 0.008 0.258-0.117 0.398-0.117 0.398s-0.141 0.148-0.414 0.172h-0.898c-1.977 0.125-3.719-1.695-3.719-1.695s-1.906-1.969-3.578-5.898c-0.109-0.258 0.008-0.383 0.008-0.383s0.117-0.148 0.445-0.148l2.141-0.016c0.203 0.031 0.344 0.141 0.344 0.141s0.125 0.086 0.187 0.25c0.352 0.875 0.805 1.672 0.805 1.672 0.781 1.609 1.313 1.883 1.617 1.719 0 0 0.398-0.242 0.312-2.188-0.031-0.703-0.227-1.023-0.227-1.023-0.18-0.242-0.516-0.312-0.664-0.336-0.117-0.016 0.078-0.297 0.336-0.422 0.383-0.187 1.062-0.195 1.867-0.187 0.633 0.008 0.813 0.047 1.055 0.102 0.742 0.18 0.492 0.867 0.492 2.523 0 0.531-0.102 1.273 0.281 1.516 0.164 0.109 0.57 0.016 1.57-1.688 0 0 0.469-0.813 0.836-1.758 0.062-0.172 0.195-0.242 0.195-0.242s0.125-0.070 0.297-0.047l2.25-0.016c0.68-0.086 0.789 0.227 0.789 0.227z"></path></svg>',
4489                    "social-email": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M15 2h-14c-0.55 0-1 0.45-1 1v10c0 0.55 0.45 1 1 1h14c0.55 0 1-0.45 1-1v-10c0-0.55-0.45-1-1-1zM14 4v0.719l-6 3.536-6-3.536v-0.719h12zM2 12v-5.54l6 3.536 6-3.536v5.54h-12z"></path></svg>',
4490                    "social-download": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M2.5 7l5.5 5.5 5.5-5.5h-3.5v-6h-4v6z"></path><path d="M14 8v6h-12v-6h-2v6.5c0 0.825 0.675 1.5 1.5 1.5h13c0.825 0 1.5-0.675 1.5-1.5v-6.5h-2z"></path></svg>',
4491                }
4492            };
4493        },
4494        register: function(setName, icons){
4495            if (_is.empty(setName) || _is.empty(icons) || !_is.string(setName) || !_is.hash(icons)) return false;
4496            this.registered[setName] = _obj.extend({}, this.registered.default, icons);
4497            return true;
4498        },
4499        get: function(name, setNameOrObject){
4500            var self = this, setName = "default",
4501                icons = _obj.extend({}, self.registered.default);
4502
4503            if (_is.string(setNameOrObject) && setNameOrObject !== "default"){
4504                setName = setNameOrObject;
4505                icons = _obj.extend(icons, self.registered[setNameOrObject]);
4506            } else if (_is.hash(setNameOrObject)){
4507                setName = "custom";
4508                icons = _obj.extend(icons, setNameOrObject);
4509            }
4510
4511            var icon = _is.string(name) && icons.hasOwnProperty(name) ? icons[name].replace(/\[ICON_CLASS]/g, self.className + "-" + name) : null,
4512                classNames = [false, name, setName].map(function(val){
4513                    return val === false ? self.className : self.className + "-" + val;
4514                }).join(" ");
4515
4516            return $(icon).addClass(classNames);
4517        },
4518        element: function(name, setNameOrObject){
4519            const self = this;
4520
4521            let setName = "default",
4522                icons = _obj.extend({}, self.registered.default);
4523
4524            if (_is.string(setNameOrObject) && setNameOrObject !== "default"){
4525                setName = setNameOrObject;
4526                icons = _obj.extend(icons, self.registered[setNameOrObject]);
4527            } else if (_is.hash(setNameOrObject)){
4528                setName = "custom";
4529                icons = _obj.extend(icons, setNameOrObject);
4530            }
4531
4532            const iconString = _is.string(name) && icons.hasOwnProperty(name) ? icons[name].replace(/\[ICON_CLASS]/g, self.className + "-" + name) : null;
4533            if ( iconString !== null ){
4534                const fragment = document.createRange().createContextualFragment(iconString);
4535                const svg = fragment.querySelector("svg");
4536                if ( svg ){
4537                    ["", "-" + name, "-" + setName].forEach(function(suffix){
4538                        svg.classList.add(self.className + suffix);
4539                    });
4540                    return svg;
4541                }
4542            }
4543            return null;
4544        }
4545    });
4546
4547    /**
4548     * @summary Icon manager for FooGallery.
4549     * @memberof FooGallery
4550     * @name icons
4551     * @type {FooGallery.Icons}
4552     */
4553    _.icons = new _.Icons();
4554
4555})(
4556    FooGallery.$,
4557    FooGallery,
4558    FooGallery.utils,
4559    FooGallery.utils.is,
4560    FooGallery.utils.obj
4561);
4562(function($, _, _utils, _is, _obj) {
4563
4564	var DATA_NAME = "__FooGallerySwipe__",
4565			TOUCH = "ontouchstart" in window,
4566			POINTER_IE10 = window.navigator.msPointerEnabled && !window.navigator.pointerEnabled && !TOUCH,
4567			POINTER = (window.navigator.pointerEnabled || window.navigator.msPointerEnabled) && !TOUCH,
4568			USE_TOUCH = TOUCH || POINTER;
4569
4570	_.Swipe = _utils.Class.extend(/** @lend FooGallery.Swipe */{
4571		/**
4572		 * @summary A utility class for handling swipe gestures on touch devices.
4573		 * @memberof FooGallery
4574		 * @constructs Swipe
4575		 * @param {Element} element - The element being bound to.
4576		 * @param {Object} options - Any options for the current instance of the class.
4577		 * @augments FooGallery.utils.Class
4578		 * @borrows FooGallery.utils.Class.extend as extend
4579		 * @borrows FooGallery.utils.Class.override as override
4580		 */
4581		construct: function(element, options){
4582			var self = this, ns = ".fgswipe";
4583			/**
4584			 * @summary The jQuery element this instance of the class is bound to.
4585			 * @memberof FooGallery.Swipe
4586			 * @name $el
4587			 * @type {jQuery}
4588			 */
4589			self.$el = $(element);
4590			/**
4591			 * @summary The options for this instance of the class.
4592			 * @memberof FooGallery.Swipe
4593			 * @name opt
4594			 * @type {FooGallery.Swipe~Options}
4595			 */
4596			self.opt = _obj.extend({
4597				threshold: 20,
4598				allowPageScroll: false,
4599				swipe: $.noop,
4600				data: {}
4601			}, options);
4602			/**
4603			 * @summary Whether or not a swipe is in progress.
4604			 * @memberof FooGallery.Swipe
4605			 * @name active
4606			 * @type {boolean}
4607			 */
4608			self.active = false;
4609			/**
4610			 * @summary The start point for the last swipe.
4611			 * @memberof FooGallery.Swipe
4612			 * @name startPoint
4613			 * @type {?FooGallery.Swipe~Point}
4614			 */
4615			self.startPoint = null;
4616			/**
4617			 * @summary The end point for the last swipe.
4618			 * @memberof FooGallery.Swipe
4619			 * @name startPoint
4620			 * @type {?FooGallery.Swipe~Point}
4621			 */
4622			self.endPoint = null;
4623			/**
4624			 * @summary The event names used by this instance of the plugin.
4625			 * @memberof FooGallery.Swipe
4626			 * @name events
4627			 * @type {{start: string, move: string, end: string, leave: string}}
4628			 */
4629			self.events = {
4630				start: (USE_TOUCH ? (POINTER ? (POINTER_IE10 ? 'MSPointerDown' : 'pointerdown') : 'touchstart') : 'mousedown') + ns,
4631				move: (USE_TOUCH ? (POINTER ? (POINTER_IE10 ? 'MSPointerMove' : 'pointermove') : 'touchmove') : 'mousemove') + ns,
4632				end: (USE_TOUCH ? (POINTER ? (POINTER_IE10 ? 'MSPointerUp' : 'pointerup') : 'touchend') : 'mouseup') + ns,
4633				leave: (USE_TOUCH ? (POINTER ? 'mouseleave' : null) : 'mouseleave') + ns
4634			};
4635		},
4636		/**
4637		 * @summary Initializes this instance of the class.
4638		 * @memberof FooGallery.Swipe
4639		 * @function init
4640		 */
4641		init: function(){
4642			var self = this;
4643			self.$el.on(self.events.start, {self: self}, self.onStart);
4644			self.$el.on(self.events.move, {self: self}, self.onMove);
4645			self.$el.on(self.events.end, {self: self}, self.onEnd);
4646			if (_is.string(self.events.leave)) self.$el.on(self.events.leave, {self: self}, self.onEnd);
4647			self.$el.data(DATA_NAME, self);
4648		},
4649		/**
4650		 * @summary Destroys this instance of the class.
4651		 * @memberof FooGallery.Swipe
4652		 * @function destroy
4653		 */
4654		destroy: function(){
4655			var self = this;
4656			self.$el.off(self.events.start, self.onStart);
4657			self.$el.off(self.events.move, self.onMove);
4658			self.$el.off(self.events.end, self.onEnd);
4659			if (_is.string(self.events.leave)) self.$el.off(self.events.leave, self.onEnd);
4660			self.$el.removeData(DATA_NAME);
4661		},
4662		/**
4663		 * @summary Gets the angle between two points.
4664		 * @memberof FooGallery.Swipe
4665		 * @function getAngle
4666		 * @param {FooGallery.Swipe~Point} pt1 - The first point.
4667		 * @param {FooGallery.Swipe~Point} pt2 - The second point.
4668		 * @returns {number}
4669		 */
4670		getAngle: function(pt1, pt2){
4671			var radians = Math.atan2(pt1.x - pt2.x, pt1.y - pt2.y),
4672					degrees = Math.round(radians * 180 / Math.PI);
4673			return 360 - (degrees < 0 ? 360 - Math.abs(degrees) : degrees);
4674		},
4675		/**
4676		 * @summary Gets the distance between two points.
4677		 * @memberof FooGallery.Swipe
4678		 * @function getDistance
4679		 * @param {FooGallery.Swipe~Point} pt1 - The first point.
4680		 * @param {FooGallery.Swipe~Point} pt2 - The second point.
4681		 * @returns {number}
4682		 */
4683		getDistance: function(pt1, pt2){
4684			var xs = pt2.x - pt1.x,
4685					ys = pt2.y - pt1.y;
4686
4687			xs *= xs;
4688			ys *= ys;
4689
4690			return Math.sqrt( xs + ys );
4691		},
4692		/**
4693		 * @summary Gets the general direction between two points and returns the result as a compass heading: N, NE, E, SE, S, SW, W, NW or NONE if the points are the same.
4694		 * @memberof FooGallery.Swipe
4695		 * @function getDirection
4696		 * @param {FooGallery.Swipe~Point} pt1 - The first point.
4697		 * @param {FooGallery.Swipe~Point} pt2 - The second point.
4698		 * @returns {string}
4699		 */
4700		getDirection: function(pt1, pt2){
4701			var self = this, angle = self.getAngle(pt1, pt2);
4702			if (angle > 337.5 || angle <= 22.5) return "N";
4703			else if (angle > 22.5 && angle <= 67.5) return "NE";
4704			else if (angle >
4704 67.5 && angle <= 112.5) return "E";
4705			else if (angle > 112.5 && angle <= 157.5) return "SE";
4706			else if (angle > 157.5 && angle <= 202.5) return "S";
4707			else if (angle > 202.5 && angle <= 247.5) return "SW";
4708			else if (angle > 247.5 && angle <= 292.5) return "W";
4709			else if (angle > 292.5 && angle <= 337.5) return "NW";
4710			return "NONE";
4711		},
4712		/**
4713		 * @summary Gets the pageX and pageY point from the supplied event whether it is for a touch or mouse event.
4714		 * @memberof FooGallery.Swipe
4715		 * @function getPoint
4716		 * @param {jQuery.Event} event - The event to parse the point from.
4717		 * @returns {FooGallery.Swipe~Point}
4718		 */
4719		getPoint: function(event){
4720			var touches;
4721			if (USE_TOUCH && !_is.empty(touches = event.originalEvent.touches || event.touches)){
4722				return {x: touches[0].pageX, y: touches[0].pageY};
4723			}
4724			if (_is.number(event.pageX) && _is.number(event.pageY)){
4725				return {x: event.pageX, y: event.pageY};
4726			}
4727			return null;
4728		},
4729		/**
4730		 * @summary Gets the offset from the supplied point.
4731		 * @memberof FooGallery.Swipe
4732		 * @function getOffset
4733		 * @param {FooGallery.Swipe~Point} pt - The point to use to calculate the offset.
4734		 * @returns {FooGallery.Swipe~Offset}
4735		 */
4736		getOffset: function(pt){
4737			var self = this, offset = self.$el.offset();
4738			return {
4739				left: pt.x - offset.left,
4740				top: pt.y - offset.top
4741			};
4742		},
4743		/**
4744		 * @summary Handles the {@link FooGallery.Swipe#events.start|start} event.
4745		 * @memberof FooGallery.Swipe
4746		 * @function onStart
4747		 * @param {jQuery.Event} event - The event object for the current event.
4748		 */
4749		onStart: function(event){
4750			var self = event.data.self, pt = self.getPoint(event);
4751			if (!_is.empty(pt)){
4752				self.active = true;
4753				self.startPoint = self.endPoint = pt;
4754			}
4755		},
4756		/**
4757		 * @summary Handles the {@link FooGallery.Swipe#events.move|move} event.
4758		 * @memberof FooGallery.Swipe
4759		 * @function onMove
4760		 * @param {jQuery.Event} event - The event object for the current event.
4761		 */
4762		onMove: function(event){
4763			var self = event.data.self, pt = self.getPoint(event);
4764			if (self.active && !_is.empty(pt)){
4765				self.endPoint = pt;
4766				if (!self.opt.allowPageScroll){
4767					event.preventDefault();
4768				} else if (_is.hash(self.opt.allowPageScroll)){
4769					var dir = self.getDirection(self.startPoint, self.endPoint);
4770					if (!self.opt.allowPageScroll.x && _utils.inArray(dir, ['NE','E','SE','NW','W','SW']) !== -1){
4771						event.preventDefault();
4772					}
4773					if (!self.opt.allowPageScroll.y && _utils.inArray(dir, ['NW','N','NE','SW','S','SE']) !== -1){
4774						event.preventDefault();
4775					}
4776				}
4777			}
4778		},
4779		/**
4780		 * @summary Handles the {@link FooGallery.Swipe#events.end|end} and {@link FooGallery.Swipe#events.leave|leave} events.
4781		 * @memberof FooGallery.Swipe
4782		 * @function onEnd
4783		 * @param {jQuery.Event} event - The event object for the current event.
4784		 */
4785		onEnd: function(event){
4786			var self = event.data.self;
4787			if (self.active){
4788				self.active = false;
4789				var info = {
4790					startPoint: self.startPoint,
4791					endPoint: self.endPoint,
4792					startOffset: self.getOffset(self.startPoint),
4793					endOffset: self.getOffset(self.endPoint),
4794					angle: self.getAngle(self.startPoint, self.endPoint),
4795					distance: self.getDistance(self.startPoint, self.endPoint),
4796					direction: self.getDirection(self.startPoint, self.endPoint)
4797				};
4798
4799				if (self.opt.threshold > 0 && info.distance < self.opt.threshold) return;
4800
4801				self.opt.swipe.apply(this, [info, self.opt.data]);
4802				self.startPoint = null;
4803				self.endPoint = null;
4804			}
4805		}
4806	});
4807
4808	/**
4809	 * @summary Expose FooGallery.Swipe as a jQuery plugin.
4810	 * @memberof external:"jQuery.fn"#
4811	 * @function fgswipe
4812	 * @param {(FooGallery.Swipe~Options|string)} [options] - The options to supply to FooGallery.Swipe or one of the supported method names.
4813	 * @returns {jQuery}
4814	 */
4815	$.fn.fgswipe = function(options){
4816		return this.each(function(){
4817			var $this = $(this), swipe = $this.data(DATA_NAME), exists = swipe instanceof _.Swipe;
4818			if (exists){
4819				if (_is.string(options) && _is.fn(swipe[options])){
4820					swipe[options]();
4821					return;
4822				} else {
4823					swipe.destroy();
4824				}
4825			}
4826			if (_is.hash(options)){
4827				swipe = new _.Swipe(this, options);
4828				swipe.init();
4829			}
4830		});
4831	};
4832
4833	/**
4834	 * @summary A simple point object containing X and Y coordinates.
4835	 * @typedef {Object} FooGallery.Swipe~Point
4836	 * @property {number} x - The X coordinate.
4837	 * @property {number} y - The Y coordinate.
4838	 */
4839
4840	/**
4841	 * @summary A simple offset object containing top and left values.
4842	 * @typedef {Object} FooGallery.Swipe~Offset
4843	 * @property {number} left - The left value.
4844	 * @property {number} top - The top value.
4845	 */
4846
4847	/**
4848	 * @summary The information object supplied as the first parameter to the {@link FooGallery.Swipe~swipeCallback} function.
4849	 * @typedef {Object} FooGallery.Swipe~Info
4850	 * @property {FooGallery.Swipe~Point} startPoint - The page X and Y coordinates where the swipe began.
4851	 * @property {FooGallery.Swipe~Point} endPoint - The page X and Y coordinates where the swipe ended.
4852	 * @property {FooGallery.Swipe~Offset} startOffset - The top and left values where the swipe began.
4853	 * @property {FooGallery.Swipe~Offset} endOffset - The top and left values where the swipe ended.
4854	 * @property {number} angle - The angle traveled from the start to the end of the swipe.
4855	 * @property {number} distance - The distance traveled from the start to the end of the swipe.
4856	 * @property {string} direction - The general direction traveled from the start to the end of the swipe: N, NE, E, SE, S, SW, W, NW or NONE if the points are the same.
4857	 */
4858
4859	/**
4860	 * @summary The callback function to execute whenever a swipe occurs.
4861	 * @callback FooGallery.Swipe~swipeCallback
4862	 * @param {FooGallery.Swipe~Info} info - The swipe info.
4863	 * @param {Object} data - Any additional data supplied when the swipe was bound.
4864	 */
4865
4866	/**
4867	 * @summary The options available for the swipe utility class.
4868	 * @typedef {Object} FooGallery.Swipe~Options
4869	 * @property {number} [threshold=20] - The minimum distance to travel before being registered as a swipe.
4870	 * @property {FooGallery.Swipe~swipeCallback} swipe - The callback function to execute whenever a swipe occurs.
4871	 * @property {Object} [data={}] - Any additional data to supply to the swipe callback.
4872	 */
4873
4874})(
4875		FooGallery.$,
4876		FooGallery,
4877		FooGallery.utils,
4878		FooGallery.utils.is,
4879		FooGallery.utils.obj
4880);
4881(function($, _, _utils, _is, _fn, _obj){
4882
4883    /**
4884     * @summary A factory for classes allowing them to be registered and created using a friendly name.
4885     * @memberof FooGallery.
4886     * @class Factory
4887     * @description This class allows other classes to register themselves for use at a later time. Depending on how you intend to use the registered classes you can also specify a load and execution order through the `priority` parameter of the {@link FooGallery.utils.Factory#register|register} method.
4888     * @augments FooGallery.utils.Class
4889     * @borrows FooGallery.utils.Class.extend as extend
4890     * @borrows FooGallery.utils.Class.override as override
4891     */
4892    _.Factory = _utils.Class.extend(/** @lends FooGallery.Factory.prototype */{
4893        /**
4894         * @ignore
4895         * @constructs
4896         **/
4897        construct: function(){
4898            /**
4899             * @summary An object containing all the required info to create a new instance of a registered class.
4900             * @typedef {Object} FooGallery.Factory~RegisteredClass
4901             * @property {string} name - The friendly name of the registered class.
4902             * @property {function} klass - The constructor for the registered class.
4903             * @property {number} priority - The priority for the registered class.
4904             */
4905
4906            /**
4907             * @summary An object containing all registered classes.
4908             * @memberof FooGallery.Factory#
4909             * @name registered
4910             * @type {Object.<string, FooGallery.Factory~RegisteredClass>}
4911             * @readonly
4912             * @example {@caption The following shows the structure of this object. The `<name>` placeholders would be the name the class was registered with.}
4913             * {
4914             * 	"<name>": {
4915             * 		"name": <string>,
4916             * 		"klass": <function>,
4917             * 		"priority": <number>
4918             * 	},
4919             * 	"<name>": {
4920             * 		"name": <string>,
4921             * 		"klass": <function>,
4922             * 		"priority": <number>
4923             * 	},
4924             * 	...
4925             * }
4926             */
4927            this.registered = {};
4928        },
4929        /**
4930         * @summary Checks if the factory contains a class registered using the supplied `name`.
4931         * @memberof FooGallery.Factory#
4932         * @function contains
4933         * @param {string} name - The name of the class to check.
4934         * @returns {boolean}
4935         * @example {@run true}
4936         * // create a new instance of the factory, this is usually exposed by the class that will be using the factory.
4937         * var factory = new FooGallery.Factory();
4938         *
4939         * // create a class to register
4940         * function Test(){}
4941         *
4942         * // register the class with the factory with the default priority
4943         * factory.register( "test", Test );
4944         *
4945         * // test if the class was registered
4946         * console.log( factory.contains( "test" ) ); // => true
4947         */
4948        contains: function(name){
4949            return !_is.undef(this.registered[name]);
4950        },
4951        /**
4952         * @summary Create a new instance of a class registered with the supplied `name` and arguments.
4953         * @memberof FooGallery.Factory#
4954         * @function make
4955         * @param {string} name - The name of the class to create.
4956         * @param {*} [arg1] - The first argument to supply to the new instance.
4957         * @param {...*} [argN] - Any number of additional arguments to supply to the new instance.
4958         * @returns {Object}
4959         * @example {@caption The following shows how to create a new instance of a registered class.}{@run true}
4960         * // create a new instance of the factory, this is usually done by the class that will be using it.
4961         * var factory = new FooGallery.Factory();
4962         *
4963         * // create a Logger class to register, this would usually be in another file
4964         * var Logger = FooGallery.Class.extend({
4965         * 	write: function( message ){
4966         * 		console.log( "Logger#write: " + message );
4967         * 	}
4968         * });
4969         *
4970         * factory.register( "logger", Logger );
4971         *
4972         * // create a new instances of the class registered as "logger"
4973         * var logger = factory.make( "logger" );
4974         * logger.write( "My message" ); // => "Logger#write: My message"
4975         */
4976        make: function(name, arg1, argN){
4977            var self = this, args = _fn.arg2arr(arguments), reg;
4978            name = args.shift();
4979            reg = self.registered[name];
4980            if (_is.hash(reg) && _is.fn(reg.klass)){
4981                return _fn.apply(reg.klass, args);
4982            }
4983            return null;
4984        },
4985        /**
4986         * @summary Gets an array of all registered names.
4987         * @memberof FooGallery.Factory#
4988         * @function names
4989         * @param {boolean} [prioritize=false] - Whether or not to order the names by the priority they were registered with.
4990         * @returns {Array.<string>}
4991         * @example {@run true}
4992         * // create a new instance of the factory, this is usually exposed by the class that will be using the factory.
4993         * var factory = new FooGallery.Factory();
4994         *
4995         * // create some classes to register
4996         * function Test1(){}
4997         * function Test2(){}
4998         *
4999         * // register the classes with the factory with the default priority
5000         * factory.register( "test-1", Test1 );
5001         * factory.register( "test-2", Test2, 1 );
5002         *
5003         * // log all registered names
5004         * console.log( factory.names() ); // => ["test-1","test-2"]
5005         * console.log( factory.names( true ) ); // => ["test-2","test-1"] ~ "test-2" appears before "test-1" as it was registered with a higher priority
5006         */
5007        names: function( prioritize ){
5008            prioritize = _is.boolean(prioritize) ? prioritize : false;
5009            var names = [], name;
5010            if (prioritize){
5011                var reg = [];
5012                for (name in this.registered){
5013                    if (!this.registered.hasOwnProperty(name)) continue;
5014                    reg.push(this.registered[name]);
5015                }
5016                reg.sort(function(a, b){ return b.priority - a.priority; });
5017                reg.forEach(function(registered){
5018                    names.push(registered.name);
5019                });
5020            } else {
5021                for (name in this.registered){
5022                    if (!this.registered.hasOwnProperty(name)) continue;
5023                    names.push(name);
5024                }
5025            }
5026            return names;
5027        },
5028        /**
5029         * @summary Registers a `klass` constructor with the factory using the given `name`.
5030         * @memberof FooGallery.Factory#
5031         * @function register
5032         * @param {string} name - The friendly name of the class.
5033         * @param {function} klass - The class constructor to register.
5034         * @param {number} [priority=0] - This determines the index for the class when using the {@link FooGallery.Factory#names|names} method, a higher value equals a lower index.
5035         * @returns {boolean} `true` if the `klass` was successfully registered.
5036         * @description Once a class is registered you can use either the {@link FooGallery.Factory#make|make} method to create new instances.
5037         * @example {@run true}
5038         * // create a new instance of the factory, this is usually exposed by the class that will be using the factory.
5039         * var factory = new FooGallery.Factory();
5040         *
5041         * // create a class to register
5042         * function Test(){}
5043         *
5044         * // register the class with the factory with the default priority
5045         * var succeeded = factory.register( "test", Test );
5046         *
5047         * console.log( succeeded ); // => true
5048         * console.log( factory.registered.hasOwnProperty( "test" ) ); // => true
5049         * console.log( factory.registered[ "test" ].name === "test" ); // => true
5050         * console.log( factory.registered[ "test" ].klass === Test ); // => true
5051         * console.log( factory.registered[ "test" ].priority === 0 ); // => true
5052         */
5053        register: function(name, klass, priority){
5054            if (!_is.string(name) || _is.empty(name) || !_is.fn(klass)) return false;
5055            priority = _is.number(priority) ? priority : 0;
5056            var current = this.registered[name];
5057            this.registered[name] = {
5058                name: name,
5059                klass: klass,
5060                priority: !_is.undef(current) ? current.priority : priority
5061            };
5062            return true;
5063        },
5064        load: function(arg1, argN){
5065            var self = this, args = _fn.arg2arr(arguments), result = [], reg = [], name;
5066            for (name in self.registered){
5067                if (!self.registered.hasOwnProperty(name)) continue;
5068                reg.push(self.registered[name]);
5069            }
5070            reg.sort(function(a, b){ return b.priority - a.priority; });
5071            reg.forEach(function(registered){
5072                var makeArgs = args.slice();
5073                makeArgs.unshift(registered.name);
5074                result.push(self.make.apply(self, makeArgs));
5075            });
5076            return result;
5077        },
5078        /**
5079         * @memberof FooGallery.Factory#
5080         * @function configure
5081         * @param {string} name
5082         * @param {object} options
5083         * @param {object} classes
5084         * @param {object} il8n
5085         */
5086        configure: function(name, options, classes, il8n){
5087            var self = this;
5088            if (self.contains(name)) {
5089                var reg = self.registered;
5090                _obj.extend(reg[name].opt, options);
5091                _obj.extend(reg[name].cls, classes);
5092                _obj.extend(reg[name].il8n, il8n);
5093            }
5094        }
5095    });
5096
5097})(
5098    // dependencies
5099    FooGallery.$,
5100    FooGallery,
5101    FooGallery.utils,
5102    FooGallery.utils.is,
5103    FooGallery.utils.fn,
5104    FooGallery.utils.obj
5105);
5106(function ($, _, _utils, _is, _fn, _obj) {
5107
5108	_.TemplateFactory = _.Factory.extend(/** @lends FooGallery.TemplateFactory */{
5109		/**
5110		 * @summary A factory for galleries allowing them to be easily registered and created.
5111		 * @memberof FooGallery
5112		 * @constructs TemplateFactory
5113		 * @description The plugin makes use of an instance of this class exposed as {@link FooGallery.template}.
5114		 * @augments FooGallery.Factory
5115		 * @borrows FooGallery.utils.Class.extend as extend
5116		 * @borrows FooGallery.utils.Class.override as override
5117		 */
5118		construct: function () {
5119			/**
5120			 * @summary An object containing all registered galleries.
5121			 * @memberof FooGallery.TemplateFactory#
5122			 * @name registered
5123			 * @type {Object.<string, Object>}
5124			 * @readonly
5125			 * @example {@caption The following shows the structure of this object. The `<name>` placeholders would be the name the class was registered with.}
5126			 * {
5127			 * 	"<name>": {
5128			 * 		"name": <string>,
5129			 * 		"klass": <function>,
5130			 * 		"test": <function>,
5131			 * 		"priority": <number>
5132			 * 	},
5133			 * 	"<name>": {
5134			 * 		"name": <string>,
5135			 * 		"klass": <function>,
5136			 * 		"test": <function>,
5137			 * 		"priority": <number>
5138			 * 	},
5139			 * 	...
5140			 * }
5141			 */
5142			this.registered = {};
5143		},
5144		/**
5145		 * @summary Registers a template constructor with the factory using the given `name` and `test` function.
5146		 * @memberof FooGallery.TemplateFactory#
5147		 * @function register
5148		 * @param {string} name - The friendly name of the class.
5149		 * @param {FooGallery.Template} template - The template constructor to register.
5150		 * @param {object} options - The default options for the template.
5151		 * @param {object} [classes={}] - The CSS classes for the template.
5152		 * @param {object} [il8n={}] - The il8n strings for the template.
5153		 * @param {number} [priority=0] - This determines the index for the class when using either the {@link FooGallery.TemplateFactory#load|load} or {@link FooGallery.TemplateFactory#names|names} methods, a higher value equals a lower index.
5154		 * @returns {boolean} `true` if the `klass` was successfully registered.
5155		 */
5156		register: function (name, template, options, classes, il8n, priority) {
5157			var self = this, result = self._super(name, template, priority);
5158			if (result) {
5159				var reg = self.registered;
5160				reg[name].opt = _is.hash(options) ? options : {};
5161				reg[name].cls = _is.hash(classes) ? classes : {};
5162				reg[name].il8n = _is.hash(il8n) ? il8n : {};
5163			}
5164			return result;
5165		},
5166		/**
5167		 * @summary Create a new instance of a registered template from the supplied `element` and `options`.
5168		 * @memberof FooGallery.TemplateFactory#
5169		 * @function make
5170		 * @param {(object|FooGallery~Options)} [options] - The options for the template. If not supplied this will fall back to using the {@link FooGallery.defaults|defaults}.
5171		 * @param {(jQuery|HTMLElement|string)} [element] - The jQuery object, HTMLElement or selector of the template element to create. If not supplied the {@link FooGallery~Options#type|type} options' value is used.
5172		 * @returns {FooGallery.Template}
5173		 */
5174		make: function (options, element) {
5175			element = _is.jq(element) ? element : $(element);
5176            options = _obj.extend({}, options, element.data("foogallery"));
5177            if (_.isMobile) {
5178                options = _obj.extend({}, options, element.data("foogalleryMobile"));
5179            }
5180			var self = this, type = self.type(options, element);
5181			if (!self.contains(type)) return null;
5182			options = self.options(type, options);
5183			return self._super(type, options, element);
5184		},
5185		type: function (options, element) {
5186			element = _is.jq(element) ? element : $(element);
5187			var self = this, type = _is.hash(options) && _is.hash(options) && _is.string(options.type) && self.contains(options.type) ? options.type : null;
5188			if (type === null && element.length > 0) {
5189				var reg = self.registered, names = self.names(true);
5190				for (var i = 0, l = names.length; i < l; i++) {
5191					if (!reg.hasOwnProperty(names[i]) || names[i] === "core") continue;
5192					var name = names[i], cls = reg[name].cls;
5193					if (!_is.string(cls.container)) continue;
5194					var selector = _utils.selectify(cls.container);
5195					if (element.is(selector)) {
5196						type = names[i];
5197						break;
5198					}
5199				}
5200			}
5201			return type;
5202		},
5203		options: function (name, options) {
5204			options = _obj.extend({type: name}, options);
5205			var self = this, reg = self.registered,
5206					def = reg["core"].opt,
5207					cls = reg["core"].cls,
5208					il8n = reg["core"].il8n;
5209
5210			if (!_is.hash(options.cls)) options.cls = {};
5211			if (!_is.hash(options.il8n)) options.il8n = {};
5212			if (!_is.undef(_.filtering)) options = _.filtering.merge(options);
5213			if (!_is.undef(_.paging)) options = _.paging.merge(options);
5214
5215			if (name !== "core" && self.contains(name)) {
5216				options = _obj.extend({}, def, reg[name].opt, options);
5217				options.cls = _obj.extend({}, cls, reg[name].cls, options.cls);
5218				options.il8n = _obj.extend({}, il8n, reg[name].il8n, options.il8n);
5219			} else {
5220				options = _obj.extend({}, def, options);
5221				options.cls = _obj.extend({}, cls, options.cls);
5222				options.il8n = _obj.extend({}, il8n, options.il8n);
5223			}
5224			return options;
5225		}
5226	});
5227
5228	/**
5229	 * @summary The factory used to register and create the various template types of FooGallery.
5230	 * @memberof FooGallery
5231	 * @name template
5232	 * @type {FooGallery.TemplateFactory}
5233	 */
5234	_.template = new _.TemplateFactory();
5235
5236})(
5237		FooGallery.$,
5238		FooGallery,
5239		FooGallery.utils,
5240		FooGallery.utils.is,
5241		FooGallery.utils.fn,
5242		FooGallery.utils.obj
5243);
5244(function ($, _, _utils, _is, _fn, _str) {
5245
5246	var instance = 0;
5247
5248	_.Template = _utils.EventClass.extend(/** @lends FooGallery.Template */{
5249		/**
5250		 * @summary The primary class for FooGallery, this controls the flow of the plugin across all templates.
5251		 * @memberof FooGallery
5252		 * @constructs Template
5253		 * @param {FooGallery.Template~Options} [options] - The options for the template.
5254		 * @param {jQuery} [element] - The jQuery object of the templates' container element. If not supplied one will be created within the `parent` element supplied to the {@link FooGallery.Template#initialize|initialize} method.
5255		 * @augments FooGallery.utils.Class
5256		 * @borrows FooGallery.utils.Class.extend as extend
5257		 * @borrows FooGallery.utils.Class.override as override
5258		 */
5259		construct: function (options, element) {
5260			var self = this;
5261			self._super();
5262			/**
5263			 * @summary An instance specific namespace to use when binding events to global objects that could be shared across multiple galleries.
5264			 * @memberof FooGallery.Template#
5265			 * @name namespace
5266			 * @type {string}
5267			 */
5268			self.namespace = ".foogallery-" + (++instance);
5269			/**
5270			 * @summary The jQuery object for the template container.
5271			 * @memberof FooGallery.Template#
5272			 * @name $el
5273			 * @type {jQuery}
5274			 */
5275			self.$el = _is.jq(element) ? element : $(element);
5276			/**
5277			 * @summary The element for the template container.
5278			 * @memberof FooGallery.Template#
5279			 * @name el
5280			 * @type {?HTMLElement}
5281			 */
5282			self.el = self.$el.get(0) || null;
5283			/**
5284			 * @summary The jQuery object for the template containers scroll parent.
5285			 * @memberof FooGallery.Template#
5286			 * @name $scrollParent
5287			 * @type {?jQuery}
5288			 */
5289			self.$scrollParent = null;
5290			/**
5291			 * @summary The options for the template.
5292			 * @memberof FooGallery.Template#
5293			 * @name opt
5294			 * @type {FooGallery.Template~Options}
5295			 */
5296			self.opt = options;
5297			/**
5298			 * @summary Any custom options for the template.
5299			 * @memberof FooGallery.Template#
5300			 * @name template
5301			 * @type {Object}
5302			 */
5303			self.template = options.template;
5304			/**
5305			 * @summary The ID for the template.
5306			 * @memberof FooGallery.Template#
5307			 * @name id
5308			 * @type {string}
5309			 */
5310			self.id = self.$el.prop("id") || options.id;
5311			/**
5312			 * @summary The CSS classes for the template.
5313			 * @memberof FooGallery.Template#
5314			 * @name cls
5315			 * @type {FooGallery.Template~CSSClasses}
5316			 */
5317			self.cls = options.cls;
5318			/**
5319			 * @summary The il8n strings for the template.
5320			 * @memberof FooGallery.Template#
5321			 * @name il8n
5322			 * @type {FooGallery.Template~il8n}
5323			 */
5324			self.il8n = options.il8n;
5325			/**
5326			 * @summary The CSS selectors for the template.
5327			 * @memberof FooGallery.Template#
5328			 * @name sel
5329			 * @type {FooGallery.Template~CSSSelectors}
5330			 */
5331			self.sel = _utils.selectify(self.cls);
5332			/**
5333			 * @summary The item manager for the template.
5334			 * @memberof FooGallery.Template#
5335			 * @name items
5336			 * @type {FooGallery.Items}
5337			 */
5338			self.items = _.components.make("items", self);
5339			/**
5340			 * @summary The page manager for the template.
5341			 * @memberof FooGallery.Template#
5342			 * @name pages
5343			 * @type {?FooGallery.Paging}
5344			 */
5345			self.pages = !_is.undef(_.paging) ? _.paging.make(options.paging.type, self) : null;
5346			/**
5347			 * @summary The page manager for the template.
5348			 * @memberof FooGallery.Template#
5349			 * @name filter
5350			 * @type {?FooGallery.Filtering}
5351			 */
5352			self.filter = !_is.undef(_.filtering) ? _.filtering.make(options.filtering.type, self) : null;
5353			/**
5354			 * @summary The state manager for the template.
5355			 * @memberof FooGallery.Template#
5356			 * @name state
5357			 * @type {FooGallery.State}
5358			 */
5359			self.state = _.components.make("state", self);
5360			/**
5361			 * @summary The promise object returned by the {@link FooGallery.Template#initialize|initialize} method.
5362			 * @memberof FooGallery.Template#
5363			 * @name _initialize
5364			 * @type {?Promise}
5365			 * @private
5366			 */
5367			self._initialize = null;
5368
5369			self._layoutTimeout = null;
5370			/**
5371			 * @memberof FooGallery.Template#
5372			 * @name _layoutWidths
5373			 * @type {Number[]}
5374			 * @private
5375			 */
5376			self._layoutWidths = [];
5377			self._resizeWidth = null;
5378			self._resizeFrame = null;
5379			/**
5380			 * @memberof FooGallery.Template#
5381			 * @name lastWidth
5382			 * @type {Number}
5383			 */
5384			self.lastWidth = 0;
5385			self.initializing = false;
5386			self.initialized = false;
5387            self.destroying = false;
5388			self.destroyed = false;
5389			self._undo = {
5390				classes: "",
5391				style: "",
5392				create: false,
5393				children: false
5394			};
5395			self.robserver = new ResizeObserver(_fn.throttle(function(entries) {
5396				if (!self.destroying && !self.destroyed && entries.length === 1 && entries[0].target === self.el){
5397					var size = _utils.getResizeObserverSize(entries[0]);
5398					if (size.width === self._resizeWidth) return;
5399					self._resizeWidth = size.width;
5400					if (self._resizeFrame !== null) cancelAnimationFrame(self._resizeFrame);
5401					self._resizeFrame = requestAnimationFrame(function(){
5402						self._resizeFrame = null;
5403						if (!self.destroying && !self.destroyed) self.layout(size.width);
5404					});
5405				}
5406			}, 50));
5407		},
5408
5409		// ################
5410		// ## Initialize ##
5411		// ################
5412
5413		/**
5414		 * @summary Initialize the template.
5415		 * @memberof FooGallery.Template#
5416		 * @function initialize
5417		 * @param {(jQuery|HTMLElement|string)} [parent] - If no element was supplied to the constructor you must supply a parent element for the template to append itself to. This can be a jQuery object, HTMLElement or a CSS selector.
5418		 * @returns {Promise.<FooGallery.Template>}
5419		 * @description Once resolved all options, objects and elements required by the template have been parsed or created and the initial load is complete.
5420		 * @fires FooGallery.Template~"pre-init.foogallery"
5421		 * @fires FooGallery.Template~"init.foogallery"
5422		 * @fires FooGallery.Template~"post-init.foogallery"
5423		 * @fires FooGallery.Template~"ready.foogallery"
5424		 */
5425		initialize: function (parent) {
5426			var self = this;
5427			if (_is.promise(self._initialize)) return self._initialize;
5428			return self._initialize = $.Deferred(function (def) {
5429				if (self.preInit(parent)){
5430					self.init().then(function(){
5431						if (self.postInit()){
5432							self.ready();
5433							def.resolve(self);
5434						} else {
5435							def.reject("post-init failed");
5436						}
5437					}).fail(def.reject);
5438				} else {
5439					def.reject("pre-init failed");
5440				}
5441			}).fail(function (err) {
5442				console.log("initialize failed", self, err);
5443				return self.destroy();
5444			}).promise();
5445		},
5446		/**
5447		 * @summary Occurs before the template is initialized.
5448		 * @memberof FooGallery.Template#
5449		 * @function preInit
5450		 * @param {(jQuery|HTMLElement|string)} [parent] - If no element was supplied to the constructor you must supply a parent element for the template to append itself to. This can be a jQuery object, HTMLElement or a CSS selector.
5451		 * @returns {boolean}
5452		 * @fires FooGallery.Template~"pre-init.foogallery"
5453		 */
5454		preInit: function (parent) {
5455			var self = this;
5456            if (self.destroying) return false;
5457			parent = _is.jq(parent) ? parent : $(parent);
5458			self.initializing = true;
5459
5460			if (parent.length === 0 && self.$el.parent().length === 0) {
5461				return false;
5462			}
5463			if (self.$el.length === 0) {
5464				self.$el = self.create();
5465				self.el = self.$el.get(0);
5466				self._undo.create = true;
5467			}
5468			if (parent.length > 0) {
5469				self.$el.appendTo(parent);
5470			}
5471
5472			var $sp;
5473			if (!_is.empty(self.opt.scrollParent) && ($sp = $(self.opt.scrollParent)).length !== 0){
5474				self.$scrollParent = $sp.is("html") ? $(document) : $sp;
5475			} else {
5476				self.$scrollParent = $(document);
5477			}
5478			self.$el.data(_.DATA_TEMPLATE, self);
5479
5480			// at this point we have our container element free of pre-existing instances so let's bind any event listeners supplied by the .on option
5481			if (!_is.empty(self.opt.on)) {
5482				self.on(self.opt.on);
5483			}
5484			self._undo.classes = self.$el.attr("class");
5485			self._undo.style = self.$el.attr("style");
5486
5487			// ensure the container has it's required CSS classes
5488			if (!self.$el.is(self.sel.container)) {
5489				self.$el.addClass(self.cls.container);
5490			}
5491			var selector = _utils.selectify(self.opt.classes);
5492			if (selector != null && !self.$el.is(selector)) {
5493				self.$el.addClass(self.opt.classes);
5494			}
5495			if (_is.hash(self.opt.responsiveClasses)) {
5496				var responsiveClasses = self.opt.responsiveClasses;
5497				if (_is.string(responsiveClasses.remove) && !_is.empty(responsiveClasses.remove)) {
5498					self.$el.removeClass(responsiveClasses.remove);
5499				}
5500				if (_is.string(responsiveClasses.add) && !_is.empty(responsiveClasses.add)) {
5501					self.$el.addClass(responsiveClasses.add);
5502				}
5503			}
5504
5505			// if the container currently has no children make them
5506			if (self.$el.children().not(self.sel.item.elem).length === 0) {
5507				self.$el.append(self.createChildren());
5508				self._undo.children = true;
5509			}
5510
5511			if (self.opt.protected){
5512				self.el.oncontextmenu = function(e){
5513					e.preventDefault();
5514					return false;
5515				};
5516			}
5517
5518			/**
5519			 * @summary Raised before the template is fully initialized.
5520			 * @event FooGallery.Template~"pre-init.foogallery"
5521			 * @type {jQuery.Event}
5522			 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
5523			 * @param {FooGallery.Template} template - The template raising the event.
5524			 * @description At this point in the initialization chain the {@link FooGallery.Template#opt|opt} property has not undergone any additional parsing and is just the result of the {@link FooGallery.defaults|default options} being extended with any user supplied ones.
5525			 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
5526			 * $(".foogallery").foogallery({
5527			 * 	on: {
5528			 * 		"pre-init.foogallery": function(event, template){
5529			 * 			// do something
5530			 * 		}
5531			 * 	}
5532			 * });
5533			 * @example {@caption Calling the `preventDefault` method on the `event` object will prevent the template being initialized.}
5534			 * $(".foogallery").foogallery({
5535			 * 	on: {
5536			 * 		"pre-init.foogallery": function(event, template){
5537			 * 			if ("some condition"){
5538			 * 				// stop the template being initialized
5539			 * 				event.preventDefault();
5540			 * 			}
5541			 * 		}
5542			 * 	}
5543			 * });
5544			 */
5545			return !self.trigger("pre-init").isDefaultPrevented();
5546		},
5547		/**
5548		 * @summary Occurs as the template is initialized.
5549		 * @memberof FooGallery.Template#
5550		 * @function init
5551		 * @returns {Promise}
5552		 * @fires FooGallery.Template~"init.foogallery"
5553		 */
5554		init: function(){
5555			var self = this;
5556			/**
5557			 * @summary Raised before the template is initialized but after any pre-initialization work is complete.
5558			 * @event FooGallery.Template~"init.foogallery"
5559			 * @type {jQuery.Event}
5560			 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
5561			 * @param {FooGallery.Template} template - The template raising the event.
5562			 * @returns {Promise} Resolved once the initialization work is complete, rejected if an error occurs or execution is prevented.
5563			 * @description At this point in the initialization chain all additional option parsing has been completed but the base components such as the items or state are not yet initialized.
5564			 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
5565			 * $(".foogallery").foogallery({
5566			 * 	on: {
5567			 * 		"init.foogallery": function(event, template){
5568			 * 			// do something
5569			 * 		}
5570			 * 	}
5571			 * });
5572			 * @example {@caption Calling the `preventDefault` method on the `event` object will prevent the template being initialized.}
5573			 * $(".foogallery").foogallery({
5574			 * 	on: {
5575			 * 		"init.foogallery": function(event, template){
5576			 * 			if ("some condition"){
5577			 * 				// stop the template being initialized
5578			 * 				event.preventDefault();
5579			 * 			}
5580			 * 		}
5581			 * 	}
5582			 * });
5583			 * @example {@caption You can also prevent the default logic and replace it with your own by calling the `preventDefault` method on the `event` object and returning a promise.}
5584			 * $(".foogallery").foogallery({
5585			 * 	on: {
5586			 * 		"init.foogallery": function(event, template){
5587			 * 			// stop the default logic
5588			 * 			event.preventDefault();
5589			 * 			// you can execute the default logic by calling the handler directly yourself
5590			 * 			// var promise = template.onInit();
5591			 * 			// replace the default logic with your own
5592			 * 			return Promise;
5593			 * 		}
5594			 * 	}
5595			 * });
5596			 */
5597			var e = self.trigger("init");
5598			if (e.isDefaultPrevented()) return _fn.reject("init default prevented");
5599			return self.items.fetch();
5600		},
5601		/**
5602		 * @summary Occurs after the template is initialized.
5603		 * @memberof FooGallery.Template#
5604		 * @function postInit
5605		 * @returns {boolean}
5606		 * @fires FooGallery.Template~"post-init.foogallery"
5607		 */
5608		postInit: function () {
5609			var self = this;
5610			if (self.destroying) return false;
5611			/**
5612			 * @summary Raised after the template is initialized but before any post-initialization work is complete.
5613			 * @event FooGallery.Template~"post-init.foogallery"
5614			 * @type {jQuery.Event}
5615			 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
5616			 * @param {FooGallery.Template} template - The template raising the event.
5617			 * @returns {Promise} Resolved once the post-initialization work is complete, rejected if an error occurs or execution is prevented.
5618			 * @description At this point in the initialization chain all options, objects and elements required by the template have been parsed or created however the initial state has not been set yet and no items have been loaded.
5619			 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
5620			 * $(".foogallery").foogallery({
5621			 * 	on: {
5622			 * 		"post-init.foogallery": function(event, template){
5623			 * 			// do something
5624			 * 		}
5625			 * 	}
5626			 * });
5627			 * @example {@caption Calling the `preventDefault` method on the `event` object will prevent the template being initialized.}
5628			 * $(".foogallery").foogallery({
5629			 * 	on: {
5630			 * 		"post-init.foogallery": function(event, template){
5631			 * 			if ("some condition"){
5632			 * 				// stop the template being initialized
5633			 * 				event.preventDefault();
5634			 * 			}
5635			 * 		}
5636			 * 	}
5637			 * });
5638			 * @example {@caption You can also prevent the default logic and replace it with your own by calling the `preventDefault` method on the `event` object and returning a promise.}
5639			 * $(".foogallery").foogallery({
5640			 * 	on: {
5641			 * 		"post-init.foogallery": function(event, template){
5642			 * 			// stop the default logic
5643			 * 			event.preventDefault();
5644			 * 			// you can execute the default logic by calling the handler directly yourself
5645			 * 			// var promise = template.onPostInit();
5646			 * 			// replace the default logic with your own
5647			 * 			return Promise;
5648			 * 		}
5649			 * 	}
5650			 * });
5651			 */
5652			var e = self.trigger("post-init");
5653			if (e.isDefaultPrevented()) return false;
5654			self.state.init();
5655			if (self.pages) self.pages.init();
5656			$(window).on("popstate" + self.namespace, {self: self}, self.onWindowPopState);
5657			self.robserver.observe(self.el);
5658			return true;
5659		},
5660		/**
5661		 * @summary Occurs once the template is ready.
5662		 * @memberof FooGallery.Template#
5663		 * @function ready
5664		 * @returns {boolean}
5665		 * @fires FooGallery.Template~"ready.foogallery"
5666		 */
5667		ready: function(){
5668			var self = this;
5669            if (self.destroying) return false;
5670			self.initializing = false;
5671			self.initialized = true;
5672			/**
5673			 * @summary Raised after the template is fully initialized and is ready to be interacted with.
5674			 * @event FooGallery.Template~"ready.foogallery"
5675			 * @type {jQuery.Event}
5676			 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
5677			 * @param {FooGallery.Template} template - The template raising the event.
5678			 * @description This event is raised after all post-initialization work such as setting the initial state and performing the first load are completed.
5679			 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
5680			 * $(".foogallery").foogallery({
5681			 * 	on: {
5682			 * 		"ready.foogallery": function(event, template){
5683			 * 			// do something
5684			 * 		}
5685			 * 	}
5686			 * });
5687			 */
5688			self.trigger("ready");
5689			return true;
5690		},
5691		/**
5692		 * @summary Create a new container element for the template returning the jQuery object.
5693		 * @memberof FooGallery.Template#
5694		 * @function create
5695		 * @returns {jQuery} A jQuery object to use as the container for the template.
5696		 * @description This method is called from within the {@link FooGallery.Template#initialize|initialize} method only if a container element is required.
5697		 * @example {@caption The below shows an example of what the returned jQuery objects' outer HTML would look like.}{@lang html}
5698		 * <div id="{options.id}" class="{options.cls.container} {options.classes}">
5699		 * </div>
5700		 */
5701		create: function () {
5702			var self = this;
5703			return $("<div/>", {"id": self.id, "class": self.cls.container}).addClass(self.opt.classes);
5704		},
5705		/**
5706		 * @summary Create any container child elements for the template returning the jQuery object.
5707		 * @memberof FooGallery.Template#
5708		 * @function createChildren
5709		 * @returns {(jQuery|jQuery[]|HTMLElement|HTMLElement[])} A jQuery object to use as the container for the template.
5710		 * @description This method is called just prior to the {@link FooGallery.Template~"preinit.foogallery"|preinit} event if the container element has no children to allow templates to add any markup required.
5711		 */
5712		createChildren: function () {
5713			return $();
5714		},
5715
5716		// #############
5717		// ## Destroy ##
5718		// #############
5719
5720		/**
5721		 * @summary Destroy the template.
5722		 * @memberof FooGallery.Template#
5723		 * @function destroy
5724		 * @param {boolean} [preserveState=false] - If set to true any existing state is left intact on the URL.
5725		 * @returns {Promise}
5726		 * @description Once this method is called it can not be stopped and the template will be destroyed.
5727		 * @fires FooGallery.Template~"destroy.foogallery"
5728		 */
5729		destroy: function (preserveState) {
5730			var self = this, _super = self._super.bind(self);
5731            if (self.destroyed) return _fn.resolved;
5732            self.destroying = true;
5733            return $.Deferred(function (def) {
5734                if (self.initializing && _is.promise(self._initialize)) {
5735                    self._initialize.always(function () {
5736                        self.destroying = false;
5737                        self.doDestroy(preserveState);
5738                        def.resolve();
5739                    });
5740                } else {
5741                    self.destroying = false;
5742                    self.doDestroy(preserveState);
5743                    def.resolve();
5744                }
5745            }).then(function(){
5746            	_super();
5747			}).promise();
5748		},
5749        doDestroy: function(preserveState){
5750		    var self = this;
5751            if (self.destroyed) return;
5752            /**
5753             * @summary Raised before the template is destroyed.
5754             * @event FooGallery.Template~"destroy.foogallery"
5755             * @type {jQuery.Event}
5756             * @param {jQuery.Event} event - The jQuery.Event object for the current event.
5757             * @param {FooGallery.Template} template - The template raising the event.
5758             * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
5759             * $(".foogallery").foogallery({
5760             * 	on: {
5761             * 		"destroy.foogallery": function(event, template){
5762             * 			// do something
5763             * 		}
5764             * 	}
5765             * });
5766             */
5767            self.trigger("destroy");
5768			if (self._resizeFrame !== null) cancelAnimationFrame(self._resizeFrame);
5769			self._resizeFrame = null;
5770			self.robserver.disconnect();
5771            $(window).off(self.namespace);
5772            self.state.destroy(preserveState);
5773            if (self.filter) self.filter.destroy();
5774            if (self.pages) self.pages.destroy();
5775            self.items.destroy();
5776            if (!_is.empty(self.opt.on)) {
5777                self.$el.off(self.opt.on);
5778            }
5779            /**
5780             * @summary Raised after the template has been destroyed.
5781             * @event FooGallery.Template~"destroyed.foogallery"
5782             * @type {jQuery.Event}
5783             * @param {jQuery.Event} event - The jQuery.Event object for the current event.
5784             * @param {FooGallery.Template} template - The template raising the event.
5785             * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
5786             * $(".foogallery").foogallery({
5787             * 	on: {
5788             * 		"destroyed.foogallery": function(event, template){
5789             * 			// do something
5790             * 		}
5791             * 	}
5792             * });
5793             */
5794            self.trigger("destroyed");
5795            self.$el.removeData(_.DATA_TEMPLATE);
5796
5797            if (_is.empty(self._undo.classes)) self.$el.removeAttr("class");
5798            else self.$el.attr("class", self._undo.classes);
5799
5800            if (_is.empty(self._undo.style)) self.$el.removeAttr("style");
5801            else self.$el.attr("style", self._undo.style);
5802
5803            if (self._undo.children) {
5804                self.destroyChildren();
5805            }
5806            if (self._undo.create) {
5807                self.$el.remove();
5808            }
5809            self.$el = self.state = self.items = self.pages = null;
5810            self.destroyed = true;
5811            self.initializing = false;
5812            self.initialized = false;
5813        },
5814		/**
5815		 * @summary If the {@link FooGallery.Template#createChildren|createChildren} method is used to generate custom elements for a template this method should also be overridden and used to destroy them.
5816		 * @memberof FooGallery.Template#
5817		 * @function destroyChildren
5818		 * @description This method is called just after the {@link FooGallery.Template~"destroyed.foogallery"|destroyed} event to allow templates to remove any markup created in the {@link FooGallery.Template#createChildren|createChildren} method.
5819		 */
5820		destroyChildren: function(){
5821			// does nothing for the base template
5822		},
5823
5824		// ################
5825		// ## Load Items ##
5826		// ################
5827
5828		/**
5829		 * @summary Gets all available items.
5830		 * @description This takes into account if paging is enabled and will return only the current pages' items.
5831		 * @memberof FooGallery.Template#
5832		 * @function getAvailable
5833		 * @returns {FooGallery.Item[]} An array of {@link FooGallery.Item|items}.
5834		 */
5835		getAvailable: function () {
5836			return this.pages ? this.pages.available() : this.items.available();
5837		},
5838
5839		// #############
5840		// ## Utility ##
5841		// #############
5842
5843		/**
5844		 * @memberof FooGallery.Template#
5845		 * @function layout
5846		 */
5847		layout: function (width) {
5848			var self = this;
5849			if (self._initialize === null) return;
5850			if (!_is.number(width)){
5851				var rect = self.el.getBoundingClientRect();
5852				width = rect.width;
5853			}
5854			if (width === 0 || self._checkWidth(width)) return;
5855
5856			self.lastWidth = width;
5857
5858			/**
5859			 * @summary Raised when the templates' {@link FooGallery.Template#layout|layout} method is called.
5860			 * @event FooGallery.Template~"layout.foogallery"
5861			 * @type {jQuery.Event}
5862			 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
5863			 * @param {FooGallery.Template} template - The template raising the event.
5864			 * @description This allows templates to perform layout if required for example when visibility changes.
5865			 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
5866			 * $(".foogallery").foogallery({
5867			 * 	on: {
5868			 * 		"layout.foogallery": function(event, template){
5869			 * 			// do something
5870			 * 		}
5871			 * 	}
5872			 * });
5873			 */
5874			self.trigger("layout", [width]);
5875		},
5876		/**
5877		 * @summary This method was added to prevent an infinite loop in the ResizeObserver.
5878		 * @memberof FooGallery.Template#
5879		 * @function _checkWidth
5880		 * @description When the viewport has no scrollbar by default and is then resized down until the gallery layout requires a scrollbar
5881		 * to show. There could be an infinite loop as follows:
5882		 * 1. No scrollbar shown, layout occurs, scrollbar is then required.
5883		 * 2. Scrollbar is shown, triggers ResizeObserver, layout occurs, scrollbar is no longer required, loops back round to #1.
5884		 * To work around this the Template now has a 100ms two width memory and if the same size appears the layout is aborted exiting the loop.
5885		 * @param {Number} width - The current width of the gallery.
5886		 * @returns {boolean} True if the layout should be allowed to proceed.
5887		 * @private
5888		 */
5889		_checkWidth: function(width){
5890			var self = this, exists;
5891			if (!(exists = (self._layoutWidths.indexOf(width) !== -1))){
5892				self._layoutWidths.unshift(width);
5893				self._layoutWidths.splice(2, self._layoutWidths.length - 2);
5894				if (self._layoutTimeout != null) clearTimeout(self._layoutTimeout);
5895				self._layoutTimeout = setTimeout(function(){
5896					self._layoutWidths.splice(0, self._layoutWidths.length);
5897				}, 100);
5898			}
5899			return exists;
5900		},
5901
5902		/**
5903		 * @summary Gets a specific type of CSS class from the template.
5904		 * @memberof FooGallery.Template#
5905		 * @function
5906		 * @name getCSSClass
5907		 * @param {string} type - The specific type of CSS class to retrieve.
5908		 * @param {string} [def=""] - The default value to return if no CSS class is found.
5909		 * @returns {string}
5910		 */
5911		getCSSClass: function(type, def){
5912			def = _is.empty(def) ? "" : def;
5913			var regex = type instanceof RegExp ? type : (_is.string(type) && this.opt.regex.hasOwnProperty(type) ? this.opt.regex[type] : null),
5914				className = (this.$el.prop("className") || ''),
5915				match = regex != null ? className.match(regex) : null;
5916			return match != null && match.length >= 2 ? match[1] : def;
5917		},
5918
5919        /**
5920         * @summary Get the BCP 47 language tag to use with APIs like Intl.NumberFormat.
5921         * @memberof FooGallery.Template#
5922         * @function
5923         * @name getLanguage
5924         * @returns {string} Returns either the `html[lang]` attribute value, otherwise the `lang` option value.
5925         */
5926        getLanguage: function(){
5927            if ( document.documentElement.lang !== '' ) {
5928                return document.documentElement.lang;
5929            }
5930            return this.opt.lang;
5931        },
5932
5933		// ###############
5934		// ## Listeners ##
5935		// ###############
5936
5937		/**
5938		 * @summary Listens for the windows popstate event and performs any actions required by the template.
5939		 * @memberof FooGallery.Template#
5940		 * @function onWindowPopState
5941		 * @param {jQuery.Event} e - The jQuery.Event object for the event.
5942		 * @private
5943		 */
5944		onWindowPopState: function (e) {
5945			var self = e.data.self, state = e.originalEvent.state;
5946			if (!_is.empty(state) && state.id === self.id) {
5947				self.state.set(state);
5948			}
5949		}
5950	});
5951
5952	_.template.register("core", _.Template, {
5953		id: null,
5954		type: "core",
5955		classes: "",
5956		responsiveClasses: {},
5957		on: {},
5958		lazy: true,
5959		items: [],
5960		scrollParent: null,
5961		delay: 0,
5962		throttle: 50,
5963		shortpixel: false,
5964		srcset: "data-srcset-fg",
5965		src: "data-src-fg",
5966		protected: false,
5967        lang: "en", // the language to use if there's no html[lang] attribute value
5968        cors: null,
5969		template: {},
5970		regex: {
5971			theme: /(?:\s|^)(fg-(?:light|dark|custom))(?:\s|$)/,
5972			loadingIcon: /(?:\s|^)(fg-loading-(?:default|bars|dots|partial|pulse|trail))(?:\s|$)/,
5973			hoverIcon: /(?:\s|^)(fg-hover-(?:zoom|zoom2|zoom3|zoom4|zoom5|plus|plus2|plus3|circle-plus|circle-plus2|square-plus|eye|external|tint))(?:\s|$)/,
5974			videoIcon: /(?:\s|^)(fg-video-(?:default|1|2|3|4))(?:\s|$)/,
5975			border: /(?:\s|^)(fg-border-(?:thin|medium|thick))(?:\s|$)/,
5976			hoverColor: /(?:\s|^)(fg-hover-(?:colorize|grayscale))(?:\s|$)/,
5977			hoverScale: /(?:\s|^)(fg-hover-scale)(?:\s|$)/,
5978			stickyVideoIcon: /(?:\s|^)(fg-video-sticky)(?:\s|$)/,
5979			insetShadow: /(?:\s|^)(fg-shadow-inset-(?:small|medium|large))(?:\s|$)/,
5980			filter: /(?:\s|^)(fg-filter-(?:1977|amaro|brannan|clarendon|earlybird|lofi|poprocket|reyes|toaster|walden|xpro2|xtreme))(?:\s|$)/
5981		}
5982	}, {
5983		container: "foogallery"
5984	}, {}, -100);
5985
5986	/**
5987	 * @summary An object containing all the core template options.
5988	 * @typedef {object} FooGallery.Template~Options
5989	 * @property {?string} [id=null] - The id for the template. This is only required if creating a template without a pre-existing container element that has an `id` attribute.
5990	 * @property {string} [type="core"] - The type of template to load. If no container element exists to parse the type from this must be supplied so the correct type of template is loaded.
5991	 * @property {string} [classes=""] - A space delimited string of any additional CSS classes to append to the container element of the template.
5992	 * @property {object} [responsiveClasses={}] - CSS classes to remove and add after the original container classes are recorded for restoration.
5993	 * @property {object} [on={}] - An object containing any template events to bind to.
5994	 * @property {boolean} [lazy=true] - Whether or not to enable lazy loading of images.
5995	 * @property {boolean} [protected=false] - Whether or not to enable basic image protection.
5996	 * @property {(FooGallery.Item~Options[]|FooGallery.Item[]| string)} [items=[]] - An array of items to load when required. A url can be provided and the items will be fetched using an ajax call, the response should be a properly formatted JSON array of {@link FooGallery.Item~Options|item} object.
5997	 * @property {string} [scrollParent=null] - The selector used to bind to the scroll parent for the gallery. If not supplied the template will attempt to find the element itself.
5998	 * @property {number} [delay=0] - The number of milliseconds to delay the initialization of a template.
5999	 * @property {number} [throttle=50] - The number of milliseconds to wait once scrolling has stopped before performing any work.
6000	 * @property {string} [src="data-src-fg"] - The name of the attribute to retrieve an images src url from.
6001	 * @property {string} [srcset="data-srcset-fg"] - The name of the attribute to retrieve an images srcset url from.
6002	 * @property {object} [template={}] - An object containing any additional custom options for the template.
6003	 * @property {FooGallery.Template~CSSClasses} [cls] - An object containing all CSS classes for the template.
6004	 * @property {FooGallery.Template~CSSSelectors} [sel] - An object containing all CSS selectors for the template.
6005	 * @property {FooGallery.Template~il8n} [il8n] - An object containing all il8n strings for the template.
6006	 * @property {FooGallery.Item~Options} [item] - An object containing the default values for all items.
6007	 * @property {FooGallery.State~Options} [state] - An object containing the state options for the template.
6008	 * @property {FooGallery.Paging~Options} [paging] - An object containing the default paging options for the template.
6009	 */
6010
6011	/**
6012	 * @summary An object containing all CSS classes for the core template.
6013	 * @typedef {object} FooGallery.Template~CSSClasses
6014	 * @property {string} [container="foogallery"] - The base CSS class names to apply to the container element.
6015	 * @property {FooGallery.Item~CSSClasses} [item] - A simple object containing the CSS classes used by an item.
6016	 */
6017
6018	/**
6019	 * @summary An object containing all il8n strings for the core template.
6020	 * @typedef {object} FooGallery.Template~il8n
6021	 */
6022
6023	/**
6024	 * @summary An object containing all CSS selectors for the core template.
6025	 * @typedef {object} FooGallery.Template~CSSSelectors
6026	 * @property {string} [container=".foogallery"] - The selector for the base CSS class names for the container element.
6027	 * @property {FooGallery.Item~CSSSelectors} [item] - An object containing the CSS selectors for an item.
6028	 * @description This object is automatically generated from a {@link FooGallery.Template~CSSClasses|classes} object and its properties mirror those except the space delimited string of class names is converted into a CSS selector.
6029	 */
6030
6031})(
6032		FooGallery.$,
6033		FooGallery,
6034		FooGallery.utils,
6035		FooGallery.utils.is,
6036		FooGallery.utils.fn,
6037		FooGallery.utils.str
6038);
6039
6040(function($, _, global){
6041	var refreshData = "__FooGalleryMobileRefresh__";
6042
6043	function removeListener(query, listener) {
6044		if (!query || !listener) {
6045			return;
6046		}
6047		if (query.removeEventListener) {
6048			query.removeEventListener("change", listener);
6049		} else if (query.removeListener) {
6050			query.removeListener(listener);
6051		}
6052	}
6053
6054	function addListener(query, listener) {
6055		if (query.addEventListener) {
6056			query.addEventListener("change", listener);
6057		} else if (query.addListener) {
6058			query.addListener(listener);
6059		}
6060	}
6061
6062	function refreshGallery(element) {
6063		var $element = $(element),
6064			current = $element.data(_.DATA_TEMPLATE),
6065			state = $element.data(refreshData);
6066
6067		if (state) {
6068			state.requested = _.isMobile;
6069			return;
6070		}
6071		if (!(current instanceof _.Template)) {
6072			return;
6073		}
6074
6075		state = { requested: _.isMobile };
6076		$element.data(refreshData, state);
6077
6078		function run() {
6079			var requested = state.requested;
6080			_.init({}, $element).always(function(){
6081				if (requested !== state.requested) {
6082					run();
6083					return;
6084				}
6085				$element.removeData(refreshData);
6086			});
6087		}
6088
6089		run();
6090	}
6091
6092	function onChange(event) {
6093		var isMobile = !!event.matches;
6094		if (_.isMobile === isMobile) {
6095			return;
6096		}
6097
6098		_.isMobile = isMobile;
6099		$(".foogallery").each(function(){
6100			refreshGallery(this);
6101		});
6102	}
6103
6104	removeListener(_.mobileMediaQuery, _.mobileMediaQueryListener);
6105
6106	var mobileSize = global.FooGallery_mobileSize || "782px";
6107	_.mobileMediaQuery = global.matchMedia
6108		? global.matchMedia("(max-width: " + mobileSize + ")")
6109		: null;
6110	_.mobileMediaQueryListener = null;
6111	_.isMobile = !!(_.mobileMediaQuery && _.mobileMediaQuery.matches);
6112
6113	if (_.mobileMediaQuery && global.FooGallery_autoMobileBreakpoint !== false) {
6114		_.mobileMediaQueryListener = onChange;
6115		addListener(_.mobileMediaQuery, onChange);
6116	}
6117})(FooGallery.$, FooGallery, globalThis);
6118
6119(function(_, _utils, _is){
6120
6121	_.Component = _utils.EventClass.extend(/** @lend FooGallery.Component */{
6122		/**
6123		 * @summary The base class for all child components of a {@link FooGallery.Template|template}.
6124		 * @constructs
6125		 * @param {FooGallery.Template} template - The template creating the component.
6126		 * @augments FooGallery.utils.EventClass
6127		 * @borrows FooGallery.utils.Class.extend as extend
6128		 * @borrows FooGallery.utils.Class.override as override
6129		 */
6130		construct: function(template){
6131			this._super();
6132			/**
6133			 * @summary The template that created this component.
6134			 * @memberof FooGallery.Component#
6135			 * @name tmpl
6136			 * @type {FooGallery.Template}
6137			 */
6138			this.tmpl = template;
6139		},
6140		/**
6141		 * @summary Destroy the component making it ready for garbage collection.
6142		 * @memberof FooGallery.Component#
6143		 * @function destroy
6144		 */
6145		destroy: function(){
6146			this.tmpl = null;
6147			this._super();
6148		}
6149	});
6150
6151	/**
6152	 * @summary A factory for registering and creating basic gallery components.
6153	 * @memberof FooGallery
6154	 * @name components
6155	 * @type {FooGallery.Factory}
6156	 */
6157	_.components = new _.Factory();
6158
6159})(
6160	FooGallery,
6161	FooGallery.utils,
6162	FooGallery.utils.is
6163);
6164(function($, _, _utils, _is, _str, _obj){
6165
6166	_.State = _.Component.extend(/** @lends FooGallery.State.prototype */{
6167		/**
6168		 * @summary This class manages all the getting and setting of its' parent templates' state.
6169		 * @memberof FooGallery
6170		 * @constructs State
6171		 * @param {FooGallery.Template} template - The template to manage the state for.
6172		 * @augments FooGallery.Component
6173		 * @borrows FooGallery.utils.Class.extend as extend
6174		 * @borrows FooGallery.utils.Class.override as override
6175		 */
6176		construct: function(template){
6177			var self = this;
6178			/**
6179			 * @ignore
6180			 * @memberof FooGallery.State#
6181			 * @function _super
6182			 */
6183			self._super(template);
6184			/**
6185			 * @summary Whether or not the history API is enabled in the current browser.
6186			 * @memberof FooGallery.State#
6187			 * @name apiEnabled
6188			 * @type {boolean}
6189			 */
6190			self.apiEnabled = !!window.history && !!history.replaceState;
6191			/**
6192			 * @summary The state specific options for the template.
6193			 * @memberof FooGallery.State#
6194			 * @name opt
6195			 * @type {FooGallery.State~Options}
6196			 */
6197			self.opt = self.tmpl.opt.state;
6198			/**
6199			 * @summary Whether or not the component is enabled.
6200			 * @memberof FooGallery.State#
6201			 * @name enabled
6202			 * @type {boolean}
6203			 */
6204			self.enabled = self.opt.enabled;
6205			/**
6206			 * @summary The current state of the template.
6207			 * @memberof FooGallery.State#
6208			 * @name current
6209			 * @type {{item: null, page: number, filter: []}}
6210			 */
6211			self.current = {
6212				filter: [],
6213				page: 0,
6214				item: null
6215			};
6216			/**
6217			 * @summary Which method of the history API to use by default when updating the state.
6218			 * @memberof FooGallery.State#
6219			 * @name pushOrReplace
6220			 * @type {string}
6221			 * @default "replace"
6222			 */
6223			self.pushOrReplace = self.isPushOrReplace(self.opt.pushOrReplace) ? self.opt.pushOrReplace : "replace";
6224
6225			self.defaultMask = "foogallery-gallery-{id}";
6226
6227			var id = _str.escapeRegExp(self.tmpl.id),
6228				masked = _str.escapeRegExp(self.getMasked()),
6229				values = _str.escapeRegExp(self.opt.values),
6230				pair = _str.escapeRegExp(self.opt.pair);
6231			/**
6232			 * @summary An object containing regular expressions used to test and parse a hash value into a state object.
6233			 * @memberof FooGallery.State#
6234			 * @name regex
6235			 * @type {{exists: RegExp, masked: RegExp, values: RegExp}}
6236			 * @readonly
6237			 * @description The regular expressions contained within this object are specific to this template and are created using the template {@link FooGallery.Template#id|id} and the delimiters from the {@link FooGallery.State#opt|options}.
6238			 */
6239			self.regex = {
6240				exists: new RegExp("^#"+id+"\\"+values+".+?"),
6241				masked: new RegExp("^#"+masked+"\\"+values+".+?"),
6242				values: new RegExp("(\\w+)"+pair+"([^"+values+"]+)", "g")
6243			};
6244			/**
6245			 * @summary Whether or not the component listens to the popstate event.
6246			 * @memberof FooGallery.State#
6247			 * @name usePopState
6248			 * @type {boolean}
6249			 */
6250			self.usePopState = self.opt.usePopState;
6251			// force context
6252			self.onPopState = self.onPopState.bind(self);
6253		},
6254		init: function(){
6255			var self = this;
6256			self.set(self.initial());
6257			if (self.enabled && self.apiEnabled && self.usePopState) window.addEventListener( 'popstate', self.onPopState );
6258		},
6259		/**
6260		 * @summary Destroy the component clearing any current state from the url and preparing it for garbage collection.
6261		 * @memberof FooGallery.State#
6262		 * @function destroy
6263		 * @param {boolean} [preserve=false] - If set to true any existing state is left intact on the URL.
6264		 */
6265		destroy: function(preserve){
6266			var self = this;
6267			if (self.enabled && self.apiEnabled && self.usePopState) window.removeEventListener( 'popstate', self.onPopState );
6268			if (!preserve) self.clear();
6269			self.opt = self.regex = {};
6270			self._super();
6271		},
6272		getIdNumber: function(){
6273			return this.tmpl.id.match(/\d+/g)[0];
6274		},
6275		getMasked: function(){
6276			var self = this, mask = _str.contains(self.opt.mask, "{id}") ? self.opt.mask : self.defaultMask;
6277			return _str.format(mask, {id: self.getIdNumber()});
6278		},
6279		/**
6280		 * @summary Check if the supplied value is `"push"` or `"replace"`.
6281		 * @memberof FooGallery.State#
6282		 * @function isPushOrReplace
6283		 * @param {*} value - The value to check.
6284		 * @returns {boolean}
6285		 */
6286		isPushOrReplace: function(value){
6287			return _utils.inArray(value, ["push","replace"]) !== -1;
6288		},
6289		/**
6290		 * @summary Check if the current url contains state for this template.
6291		 * @memberof FooGallery.State#
6292		 * @function exists
6293		 * @returns {boolean}
6294		 */
6295		exists: function(){
6296			this.regex.values.lastIndex = 0; // reset the index as we use the g flag
6297			return (this.regex.exists.test(location.hash) || this.regex.masked.test(location.hash)) && this.regex.values.test(location.hash);
6298		},
6299		/**
6300		 * @summary Parse the current url returning an object containing all values for the template.
6301		 * @memberof FooGallery.State#
6302		 * @function parse
6303		 * @returns {object}
6304		 * @description This method always returns an object, if successful the object contains properties otherwise it is just a plain empty object. For this method to be successful the current template {@link FooGallery.Template#id|id} must match the one from the url.
6305		 */
6306		parse: function(){
6307			var self = this, tmpl = self.tmpl, state = {};
6308			if (self.exists()){
6309				if (self.enabled){
6310					state.id = self.tmpl.id;
6311					self.regex.values.lastIndex = 0;
6312					var pairs = location.hash.match(self.regex.values);
6313					$.each(pairs, function(i, pair){
6314						var parts = pair.split(self.opt.pair), val;
6315						if (parts.length === 2){
6316							switch(parts[0]){
6317								case self.opt.itemKey:
6318									val = tmpl.items.fromHash(parts[1]);
6319									if (val !== null) state.item = val;
6320									break;
6321								case self.opt.pageKey:
6322									if (tmpl.pages){
6323										val = tmpl.pages.fromHash(parts[1]);
6324										if (val !== null) state.page = val;
6325									}
6326									break;
6327								case self.opt.filterKey:
6328									if (tmpl.filter){
6329										val = tmpl.filter.fromHash(parts[1]);
6330										if (val !== null) state.filter = val;
6331									}
6332									break;
6333							}
6334						}
6335					});
6336				} else {
6337					// if we're here it means there is a hash on the url but the option is disabled so remove it
6338					if (self.apiEnabled){
6339						history.replaceState(null, "", location.pathname + location.search);
6340					} else {
6341						location.hash = "#";
6342					}
6343				}
6344			}
6345			return state;
6346		},
6347		/**
6348		 * @summary Converts the supplied state object into a string value to use as a hash.
6349		 * @memberof FooGallery.State#
6350		 * @function hashify
6351		 * @param {object} state - The object to hashify.
6352		 * @returns {string}
6353		 */
6354		hashify: function(state){
6355			var self = this, tmpl = self.tmpl;
6356			if (_is.hash(state)){
6357				var hash = [], val = tmpl.items.toHash(state.item);
6358				if (val !== null) hash.push(self.opt.itemKey + self.opt.pair + val);
6359
6360				if (!!tmpl.filter){
6361					val = tmpl.filter.toHash(state.filter);
6362					if (val !== null) hash.push(self.opt.filterKey + self.opt.pair + val);
6363				}
6364				if (!!tmpl.pages){
6365					val = tmpl.pages.toHash(state.page);
6366					if (val !== null) hash.push(self.opt.pageKey + self.opt.pair + val);
6367				}
6368				if (hash.length > 0){
6369					hash.unshift("#"+self.getMasked());
6370				}
6371				return hash.join(self.opt.values);
6372			}
6373			return "";
6374		},
6375		/**
6376		 * @summary Replace the current state with the one supplied.
6377		 * @memberof FooGallery.State#
6378		 * @function replace
6379		 * @param {object} state - The state to replace the current with.
6380		 */
6381		replace: function(state){
6382			var self = this;
6383			if (self.enabled && self.apiEnabled){
6384				state.id = self.tmpl.id;
6385				var hash = self.hashify(state), empty = _is.empty(hash), hs = _obj.extend({}, state, {item: state.item instanceof _.Item ? state.item.id : state.item});
6386				history.replaceState(empty ? null : hs, "", empty ? location.pathname + location.search : hash);
6387			}
6388		},
6389		/**
6390		 * @summary Push the supplied `state` into the browser history.
6391		 * @memberof FooGallery.State#
6392		 * @function push
6393		 * @param {object} state - The state to push.
6394		 */
6395		push: function(state){
6396			var self = this;
6397			if (self.enabled && self.apiEnabled){
6398				state.id = self.tmpl.id;
6399				var hash = self.hashify(state), empty = _is.empty(hash), hs = _obj.extend({}, state, {item: state.item instanceof _.Item ? state.item.id : state.item});
6400				history.pushState(empty ? null : hs, "", empty ? location.pathname + location.search : hash);
6401			}
6402		},
6403		/**
6404		 * @summary Update the browser history using the supplied `state`.
6405		 * @memberof FooGallery.State#
6406		 * @function update
6407		 * @param {object} state - The state to update.
6408		 * @param {string} [pushOrReplace] - The method to use to update the state. If not supplied this falls back to the value of the {@link FooGallery.State#pushOrReplace|pushOrReplace} property.
6409		 */
6410		update: function(state, pushOrReplace){
6411			var self = this;
6412			if (self.enabled && self.apiEnabled){
6413				pushOrReplace = self.isPushOrReplace(pushOrReplace) ? pushOrReplace : self.pushOrReplace;
6414				self[pushOrReplace](state);
6415			}
6416		},
6417		/**
6418		 * @summary Clear the template state from the current browser history if it exists.
6419		 * @memberof FooGallery.State#
6420		 * @function clear
6421		 */
6422		clear: function(){
6423			if (this.exists()) this.replace({});
6424		},
6425		/**
6426		 * @summary Get the initial start up state of the template.
6427		 * @memberof FooGallery.State#
6428		 * @function initial
6429		 * @returns {FooGallery~State}
6430		 * @description This method returns an initial start up state from the template options.
6431		 */
6432		initial: function(){
6433			var self = this, state = self.parse();
6434			if (_is.empty(state)){
6435				return self.get();
6436			}
6437			return _obj.extend({ filter: [], page: 1, item: null }, state);
6438		},
6439		/**
6440		 * @summary Get the current state of the template.
6441		 * @memberof FooGallery.State#
6442		 * @function get
6443		 * @param {FooGallery.Item} [item] - If supplied the items' {@link FooGallery.Item#id|id} is included in the resulting state object.
6444		 * @returns {FooGallery~State}
6445		 * @description This method does not parse the history or url it returns the current state of the template itself. To parse the current url use the {@link FooGallery.State#parse|parse} method instead.
6446		 */
6447		get: function(item){
6448			var self = this, tmpl = self.tmpl, state = {}, val;
6449			if (item instanceof _.Item) state.item = item;
6450			if (!!tmpl.filter){
6451				val = tmpl.filter.getState();
6452				if (val !== null) state.filter = val;
6453			}
6454			if (!!tmpl.pages){
6455				val = tmpl.pages.getState();
6456				if (val !== null) state.page = val;
6457			}
6458			return _obj.extend({ filter: [], page: 1, item: null }, state);
6459		},
6460		/**
6461		 * @summary Set the current state of the template.
6462		 * @memberof FooGallery.State#
6463		 * @function set
6464		 * @param {FooGallery~State} state - The state to set the template to.
6465		 * @description This method does not set the history or url it sets the current state of the template itself. To update the url use the {@link FooGallery.State#update|update} method instead.
6466		 */
6467		set: function(state){
6468			var self = this, tmpl = self.tmpl;
6469			if (_is.hash(state)){
6470				var obj = _obj.extend({ filter: [], page: 1, item: null }, state);
6471				tmpl.items.reset();
6472				var e = tmpl.trigger("before-state", [obj]);
6473				if (!e.isDefaultPrevented()){
6474					if (!!tmpl.filter){
6475						tmpl.filter.setState(obj);
6476					}
6477					if (!!tmpl.pages){
6478						tmpl.pages.setState(obj);
6479					} else {
6480						var available = tmpl.items.available();
6481						if (!tmpl.items.isAll(available)){
6482							var notAvailable = tmpl.items.not(available);
6483							tmpl.items.detach(notAvailable);
6484						}
6485						tmpl.items.create(available, true);
6486					}
6487					if (obj.item instanceof _.Item){
6488						if (self.opt.scrollTo) {
6489							obj.item.scrollTo();
6490						}
6491						if (!_is.empty(state.item)){
6492							state.item = null;
6493							self.replace(state);
6494						}
6495					}
6496					self.current = obj;
6497					tmpl.trigger("after-state", [obj]);
6498				}
6499			}
6500		},
6501		getItemHash: function(item) {
6502			const self = this;
6503			if (self.enabled && item instanceof _.Item) {
6504				let state = self.get(item);
6505				return self.hashify(state);
6506			}
6507			return '';
6508		},
6509		onPopState: function(e){
6510			var self = this, parsed = self.parse();
6511			if ( Object.keys( parsed ).length ){
6512				self.set( parsed );
6513			}
6514		}
6515	});
6516
6517	_.template.configure("core", {
6518		state: {
6519			enabled: false,
6520			scrollTo: true,
6521			pushOrReplace: "replace",
6522			mask: "foogallery-gallery-{id}",
6523			usePopState: true,
6524			values: "/",
6525			pair: ":",
6526			array: "+",
6527			arraySeparator: ",",
6528			itemKey: "i",
6529			filterKey: "f",
6530			pageKey: "p"
6531		}
6532	});
6533
6534	// register the component
6535	_.components.register("state", _.State);
6536
6537	/**
6538	 * @summary An object containing the state options for the template.
6539	 * @typedef {object} FooGallery.State~Options
6540	 * @property {boolean} [enabled=false] - Whether state is enabled for the template.
6541	 * @property {boolean} [scrollTo=true] - Whether the page is scrolled to the current state item.
6542	 * @property {string} [pushOrReplace="replace"] - Which method of the history API to use by default when updating the state.
6543	 * @property {string} [mask="foogallery-gallery-{id}"] - The mask used to generate the full ID from just the ID number.
6544	 * @property {boolean} [usePopState=true] - Whether state listens to the 'popstate' event and updates the gallery.
6545	 * @property {string} [values="/"] - The delimiter used between key value pairs in the hash.
6546	 * @property {string} [pair=":"] - The delimiter used between a key and a value in the hash.
6547	 * @property {string} [array="+"] - The delimiter used for array values in the hash.
6548	 * @property {string} [arraySeparator=","] - The delimiter used to separate multiple array values in the hash.
6549	 */
6550
6551	/**
6552	 * @summary An object used to store the state of a template.
6553	 * @typedef {object} FooGallery~State
6554	 * @property {number} [page] - The current page number.
6555	 * @property {string[]} [filter] - The current filter array.
6556	 * @property {?FooGallery.Item} [item] - The currently selected item.
6557	 */
6558
6559})(
6560	FooGallery.$,
6561	FooGallery,
6562	FooGallery.utils,
6563	FooGallery.utils.is,
6564	FooGallery.utils.str,
6565	FooGallery.utils.obj
6566);
6567(function ($, _, _utils, _is, _fn, _obj) {
6568
6569	_.Items = _.Component.extend(/** @lends FooGallery.Items.prototype */{
6570		/**
6571		 * @summary This class controls everything related to items and serves as the base class for the various paging types.
6572		 * @memberof FooGallery
6573		 * @constructs Items
6574		 * @param {FooGallery.Template} template - The template for this component.
6575		 * @augments FooGallery.Component
6576		 * @borrows FooGallery.utils.Class.extend as extend
6577		 * @borrows FooGallery.utils.Class.override as override
6578		 */
6579		construct: function (template) {
6580			var self = this;
6581			self.ALLOW_CREATE = true;
6582			self.ALLOW_APPEND = true;
6583			self.LAYOUT_AFTER_LOAD = true;
6584			/**
6585			 * @ignore
6586			 * @memberof FooGallery.Items#
6587			 * @function _super
6588			 */
6589			self._super(template);
6590			self._typeRegex = /(?:^|\s)?fg-type-(.*?)(?:$|\s)/;
6591			self._fetched = null;
6592			self._all = [];
6593			self._available = [];
6594			self._unavailable = [];
6595			self._observed = new Map();
6596
6597			// add the .all caption selector
6598			var cls = self.tmpl.cls.item.caption;
6599			self.tmpl.sel.item.caption.all = _utils.selectify([cls.elem, cls.inner, cls.title, cls.description]);
6600
6601			self._wait = [];
6602			self._layoutTimeout = null;
6603			self.iobserver = new IntersectionObserver(function(entries){
6604				if (!self.tmpl.destroying && !self.tmpl.destroyed){
6605					if ( self.LAYOUT_AFTER_LOAD ) clearTimeout(self._layoutTimeout);
6606					entries.forEach(function(entry){
6607						if (entry.isIntersecting){
6608							var item = self._observed.get(entry.target);
6609							if (item instanceof _.Item){
6610								self._wait.push(item.load());
6611							}
6612						}
6613					});
6614					if ( self.LAYOUT_AFTER_LOAD ){
6615						self._layoutTimeout = setTimeout(function(){
6616							if (self._wait.length > 0){
6617								_fn.allSettled(self._wait.splice(0)).then(function(){
6618									self.tmpl.layout();
6619								});
6620							}
6621						}, 100);
6622					}
6623				}
6624			});
6625		},
6626		fromHash: function(hash){
6627			return this.find(this._all, function(item){ return item.id === hash; });
6628		},
6629		toHash: function(value){
6630			return value instanceof _.Item ? value.id : null;
6631		},
6632		destroy: function () {
6633			var self = this, items = self.all(), destroyed = [];
6634			self.iobserver.disconnect();
6635			if (items.length > 0) {
6636				/**
6637				 * @summary Raised before the template destroys its' items.
6638				 * @event FooGallery.Template~"destroy-items.foogallery"
6639				 * @type {jQuery.Event}
6640				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
6641				 * @param {FooGallery.Template} template - The template raising the event.
6642				 * @param {FooGallery.Item[]} items - The array of items about to be destroyed.
6643				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
6644				 * $(".foogallery").foogallery({
6645				 * 	on: {
6646				 * 		"destroy-items.foogallery": function(event, template, items){
6647				 * 			// do something
6648				 * 		}
6649				 * 	}
6650				 * });
6651				 */
6652				self.tmpl.trigger("destroy-items", [items]);
6653				destroyed = $.map(items, function (item) {
6654					return item.destroy() ? item : null;
6655				});
6656				/**
6657				 * @summary Raised after the template has destroyed items.
6658				 * @event FooGallery.Template~"destroyed-items.foogallery"
6659				 * @type {jQuery.Event}
6660				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
6661				 * @param {FooGallery.Template} template - The template raising the event.
6662				 * @param {FooGallery.Item[]} items - The array of items destroyed.
6663				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
6664				 * $(".foogallery").foogallery({
6665				 * 	on: {
6666				 * 		"destroyed-items.foogallery": function(event, template, items){
6667				 * 			// do something
6668				 * 		}
6669				 * 	}
6670				 * });
6671				 */
6672				if (destroyed.length > 0) self.tmpl.trigger("destroyed-items", [destroyed]);
6673				// should we handle a case where the destroyed.length != items.length??
6674			}
6675			self._fetched = null;
6676			self._all = [];
6677			self._available = [];
6678			self._unavailable = [];
6679			self._observed.clear();
6680			self._super();
6681		},
6682		fetch: function (refresh) {
6683			var self = this;
6684			if (!refresh && _is.promise(self._fetched)) return self._fetched;
6685			var itemsId = self.tmpl.id + "_items",
6686				selectors = self.tmpl.sel,
6687				option = self.tmpl.opt.items,
6688				def = $.Deferred();
6689
6690			var items = self.make(self.tmpl.$el.find(selectors.item.elem));
6691
6692			if (!_is.empty(option)) {
6693				if (_is.array(option)) {
6694					items.push.apply(items, self.make(option));
6695					def.resolve(items);
6696				} else if (_is.string(option)) {
6697					$.get(option).then(function (response) {
6698						items.push.apply(items, self.make(response));
6699						def.resolve(items);
6700					}, function (jqXHR, textStatus, errorThrown) {
6701						console.log("FooGallery: GET items error.", option, jqXHR, textStatus, errorThrown);
6702						def.resolve(items);
6703					});
6704				} else {
6705					def.resolve(items);
6706				}
6707			} else {
6708				if (_is.array(window[itemsId])) {
6709					items.push.apply(items, self.make(window[itemsId]));
6710				}
6711				def.resolve(items);
6712			}
6713			def.then(function (items) {
6714				self.setAll(items);
6715			});
6716			return self._fetched = def.promise();
6717		},
6718		toJSON: function(all){
6719			var items = all ? this.all() : this.available();
6720			return items.map(function(item){
6721				return item.toJSON();
6722			});
6723		},
6724		all: function () {
6725			return this._all.slice();
6726		},
6727		count: function (all) {
6728			return all ? this.all().length : this.available().length;
6729		},
6730		available: function (where) {
6731			if (_is.fn(where)){
6732				return this._available.filter(where, this);
6733			}
6734			return this._available.slice();
6735		},
6736		unavailable: function (where) {
6737			if (_is.fn(where)){
6738				return this._unavailable.filter(where, this);
6739			}
6740			return this._unavailable.slice();
6741		},
6742		setAll: function (items) {
6743			this._all = _is.array(items) ? items : [];
6744			this._all.forEach(function(item, i){
6745				item.index = i;
6746				if (_is.empty(item.id)) item.id = (i + 1) + "";
6747			});
6748			this._available = this.all();
6749			this._unavailable = [];
6750		},
6751		setAvailable: function (items) {
6752			var self = this;
6753			self._available = _is.array(items) ? items : [];
6754			if (self._all.length !== self._available.length){
6755				self._unavailable = self._all.filter(function(item){
6756					return self._available.indexOf(item) === -1;
6757				});
6758			} else {
6759				self._unavailable = [];
6760			}
6761		},
6762		reset: function () {
6763			this.setAvailable(this.all());
6764		},
6765		find: function(items, where){
6766			where = _is.fn(where) ? where : function(){ return true; };
6767			if (_is.array(items)){
6768				for (var i = 0, l = items.length; i < l; i++){
6769					if (where.call(this, items[i]) === true){
6770						return items[i];
6771					}
6772				}
6773			}
6774			return null;
6775		},
6776		not: function(items){
6777			var all = this.all();
6778			if (_is.array(items)){
6779				return all.filter(function(item){
6780					return items.indexOf(item) === -1;
6781				});
6782			}
6783			return all;
6784		},
6785		isAll: function(items){
6786			if (_is.array(items)){
6787				return this._all.length === items.length;
6788			}
6789			return false;
6790		},
6791		first: function(where){
6792			return this.find(this._available, where);
6793		},
6794		last: function(where){
6795			return this.find(this._available.slice().reverse(), where);
6796		},
6797		next: function(item, where, loop){
6798			if (!(item instanceof _.Item)) return null;
6799			loop = _is.boolean(loop) ? loop : false;
6800			var items = this._available.slice(),
6801				index = items.indexOf(item);
6802			if (index !== -1){
6803				var remainder = items.slice(0, index);
6804				items = items.slice(index + 1);
6805				if (loop){
6806					items = items.concat(remainder);
6807				}
6808				return this.find(items, where);
6809			}
6810			return null;
6811		},
6812		prev: function(item, where, loop){
6813			if (!(item instanceof _.Item)) return null;
6814			loop = _is.boolean(loop) ? loop : false;
6815			var items = this._available.slice().reverse(),
6816				index = items.indexOf(item);
6817			if (index !== -1){
6818				var remainder = items.slice(0, index);
6819				items = items.slice(index + 1);
6820				if (loop){
6821					items = items.concat(remainder);
6822				}
6823				return this.find(items, where);
6824			}
6825			return null;
6826		},
6827		get: function( indexOrElement, all ){
6828			const self = this;
6829			const items = all ? self._all : self._available;
6830			if ( _is.number( indexOrElement ) ){
6831				if ( indexOrElement >= 0 && indexOrElement < self._all.length ){
6832					return items[ indexOrElement ];
6833				}
6834				return null;
6835			}
6836			return self.find( items, function( item ){
6837				return item.el === indexOrElement;
6838			} );
6839		},
6840		indexOf: function( item, all ){
6841			return ( all ? this._all : this._available ).indexOf( item );
6842		},
6843		/**
6844		 * @summary Filter the supplied `items` and return only those that can be created.
6845		 * @memberof FooGallery.Items#
6846		 * @function creatable
6847		 * @param {FooGallery.Item[]} items - The items to filter.
6848		 * @returns {FooGallery.Item[]}
6849		 */
6850		creatable: function (items) {
6851			return this.ALLOW_CREATE && _is.array(items) ? $.map(items, function (item) {
6852						return item instanceof _.Item && !item.isCreated ? item : null;
6853					}) : [];
6854		},
6855		/**
6856		 * @summary Filter the supplied `items` and return only those that can be appended.
6857		 * @memberof FooGallery.Items#
6858		 * @function appendable
6859		 * @param {FooGallery.Item[]} items - The items to filter.
6860		 * @returns {FooGallery.Item[]}
6861		 */
6862		appendable: function (items) {
6863			return this.ALLOW_APPEND && _is.array(items) ? $.map(items, function (item) {
6864						return item instanceof _.Item && item.isCreated && !item.isAttached ? item : null;
6865					}) : [];
6866		},
6867		/**
6868		 * @summary Filter the supplied `items` and return only those that can be detached.
6869		 * @memberof FooGallery.Items#
6870		 * @function detachable
6871		 * @param {FooGallery.Item[]} items - The items to filter.
6872		 * @returns {FooGallery.Item[]}
6873		 */
6874		detachable: function (items) {
6875			return _is.array(items) ? $.map(items, function (item) {
6876						return item instanceof _.Item && item.isCreated && item.isAttached ? item : null;
6877					}) : [];
6878		},
6879		/**
6880		 * @summary Get a single jQuery object containing all the supplied items' elements.
6881		 * @memberof FooGallery.Items#
6882		 * @function jquerify
6883		 * @param {FooGallery.Item[]} items - The items to get a jQuery object for.
6884		 * @returns {jQuery}
6885		 */
6886		jquerify: function (items) {
6887			return $($.map(items, function (item) {
6888				return item.$el.get();
6889			}));
6890		},
6891		/**
6892		 * @summary Makes a jQuery object, NodeList or an array of elements or item options, into an array of {@link FooGallery.Item|item} objects.
6893		 * @memberof FooGallery.Items#
6894		 * @function make
6895		 * @param {(jQuery|NodeList|Node[]|FooGallery.Item~Options[])} items - The value to convert into an array of items.
6896		 * @returns {FooGallery.Item[]} The array of items successfully made.
6897		 * @fires FooGallery.Template~"make-items.foogallery"
6898		 * @fires FooGallery.Template~"made-items.foogallery"
6899		 * @fires FooGallery.Template~"parsed-items.foogallery"
6900		 */
6901		make: function (items) {
6902			var self = this, made = [];
6903			if (_is.jq(items) || _is.array(items)) {
6904				var parsed = [], arr = $.makeArray(items);
6905				if (arr.length === 0) return made;
6906				/**
6907				 * @summary Raised before the template makes an array of elements or item options into an array of {@link FooGallery.Item|item} objects.
6908				 * @event FooGallery.Template~"make-items.foogallery"
6909				 * @type {jQuery.Event}
6910				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
6911				 * @param {FooGallery.Template} template - The template raising the event.
6912				 * @param {(HTMLElement[]|FooGallery.Item~Options[])} items - The array of Nodes or item options.
6913				 * @returns {(HTMLElement[]|FooGallery.Item~Options[])} A filtered list of items to make.
6914				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
6915				 * $(".foogallery").foogallery({
6916				 * 	on: {
6917				 * 		"make-items.foogallery": function(event, template, items){
6918				 * 			// do something
6919				 * 		}
6920				 * 	}
6921				 * });
6922				 * @example {@caption Calling the `preventDefault` method on the `event` object will prevent any items being made.}
6923				 * $(".foogallery").foogallery({
6924				 * 	on: {
6925				 * 		"make-items.foogallery": function(event, template, items){
6926				 * 			if ("some condition"){
6927				 * 				// stop any items being made
6928				 * 				event.preventDefault();
6929				 * 			}
6930				 * 		}
6931				 * 	}
6932				 * });
6933				 */
6934				var e = self.tmpl.trigger("make-items", [arr]);
6935				if (!e.isDefaultPrevented()) {
6936					made = $.map(arr, function (obj) {
6937						var type = self.type(obj), opt = _obj.extend(_is.hash(obj) ? obj : {}, {type: type});
6938						var item = _.components.make(type, self.tmpl, opt);
6939						if (_is.element(obj)) {
6940							if (item.parse(obj)) {
6941								parsed.push(item);
6942								if (!self.ALLOW_APPEND) item.detach();
6943								return item;
6944							}
6945							return null;
6946						}
6947						return item;
6948					});
6949				}
6950
6951				/**
6952				 * @summary Raised after the template has made an array of {@link FooGallery.Item|item} objects.
6953				 * @event FooGallery.Template~"made-items.foogallery"
6954				 * @type {jQuery.Event}
6955				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
6956				 * @param {FooGallery.Template} template - The template raising the event.
6957				 * @param {FooGallery.Item[]} items - The array of items made, this includes parsed items.
6958				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
6959				 * $(".foogallery").foogallery({
6960				 * 	on: {
6961				 * 		"made-items.foogallery": function(event, template, items){
6962				 * 			// do something
6963				 * 		}
6964				 * 	}
6965				 * });
6966				 */
6967				if (made.length > 0) self.tmpl.trigger("made-items", [made]);
6968
6969				/**
6970				 * @summary Raised after the template has parsed any elements into an array of {@link FooGallery.Item|item} objects.
6971				 * @event FooGallery.Template~"parsed-items.foogallery"
6972				 * @type {jQuery.Event}
6973				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
6974				 * @param {FooGallery.Template} template - The template raising the event.
6975				 * @param {FooGallery.Item[]} items - The array of items parsed.
6976				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
6977				 * $(".foogallery").foogallery({
6978				 * 	on: {
6979				 * 		"parsed-items.foogallery": function(event, template, items){
6980				 * 			// do something
6981				 * 		}
6982				 * 	}
6983				 * });
6984				 */
6985				if (parsed.length > 0) self.tmpl.trigger("parsed-items", [parsed]);
6986			}
6987			return made;
6988		},
6989		type: function (objOrElement) {
6990			var self = this, type;
6991			if (_is.hash(objOrElement)) {
6992				type = objOrElement.type;
6993			} else if (_is.element(objOrElement)) {
6994				var match = objOrElement.className.match(self._typeRegex);
6995				if (match !== null && match.length === 2){
6996					type = match[1];
6997				}
6998				// var $el = $(objOrElement), item = this.tmpl.sel.item;
6999				// type = $el.find(item.anchor).data("type");
7000			}
7001			return _is.string(type) && _.components.contains(type) ? type : "image";
7002		},
7003		/**
7004		 * @summary Create each of the supplied {@link FooGallery.Item|`items`} elements.
7005		 * @memberof FooGallery.Items#
7006		 * @function create
7007		 * @param {FooGallery.Item[]} items - The array of items to create.
7008		 * @param {boolean} [append=false] - Whether or not to automatically append the item after it is created.
7009		 * @returns {FooGallery.Item[]} The array of items that were created or if `append` is `true` the array of items that were appended.
7010		 * @description This will only create and/or append items that are not already created and/or appended so it is safe to call without worrying about the items' pre-existing state.
7011		 * @fires FooGallery.Template~"create-items.foogallery"
7012		 * @fires FooGallery.Template~"created-items.foogallery"
7013		 * @fires FooGallery.Template~"append-items.foogallery"
7014		 * @fires FooGallery.Template~"appended-items.foogallery"
7015		 */
7016		create: function (items, append) {
7017			var self = this, created = [], creatable = self.creatable(items);
7018			if (creatable.length > 0) {
7019				/**
7020				 * @summary Raised before the template creates the `items` elements.
7021				 * @event FooGallery.Template~"create-items.foogallery"
7022				 * @type {jQuery.Event}
7023				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
7024				 * @param {FooGallery.Template} template - The template raising the event.
7025				 * @param {FooGallery.Item[]} items - The array of items to create.
7026				 * @param {boolean} [append=false] - Whether or not to automatically append the item after it is created.
7027				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
7028				 * $(".foogallery").foogallery({
7029				 * 	on: {
7030				 * 		"create-items.foogallery": function(event, template, items){
7031				 * 			// do something
7032				 * 		}
7033				 * 	}
7034				 * });
7035				 * @example {@caption Calling the `preventDefault` method on the `event` object will prevent any items being created.}
7036				 * $(".foogallery").foogallery({
7037				 * 	on: {
7038				 * 		"create-items.foogallery": function(event, template, items){
7039				 * 			if ("some condition"){
7040				 * 				// stop any items being created
7041				 * 				event.preventDefault();
7042				 * 			}
7043				 * 		}
7044				 * 	}
7045				 * });
7046				 */
7047				var e = self.tmpl.trigger("create-items", [creatable]);
7048				if (!e.isDefaultPrevented()) {
7049					created = $.map(creatable, function (item) {
7050						return item.create() ? item : null;
7051					});
7052				}
7053				/**
7054				 * @summary Raised after the template has created the `items` elements.
7055				 * @event FooGallery.Template~"created-items.foogallery"
7056				 * @type {jQuery.Event}
7057				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
7058				 * @param {FooGallery.Template} template - The template raising the event.
7059				 * @param {FooGallery.Item[]} items - The array of items created.
7060				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
7061				 * $(".foogallery").foogallery({
7062				 * 	on: {
7063				 * 		"created-items.foogallery": function(event, template, items){
7064				 * 			// do something
7065				 * 		}
7066				 * 	}
7067				 * });
7068				 */
7069				if (created.length > 0) self.tmpl.trigger("created-items", [created]);
7070			}
7071			if (_is.boolean(append) ? append : false) return self.append(items);
7072			return created;
7073		},
7074		/**
7075		 * @summary Append each of the supplied {@link FooGallery.Item|`items`} to the template.
7076		 * @memberof FooGallery.Items#
7077		 * @function append
7078		 * @param {FooGallery.Item[]} items - The array of items to append.
7079		 * @returns {FooGallery.Item[]} The array of items that were appended.
7080		 * @fires FooGallery.Template~"append-items.foogallery"
7081		 * @fires FooGallery.Template~"appended-items.foogallery"
7082		 */
7083		append: function (items) {
7084			var self = this, appended = [], appendable = self.appendable(items);
7085			if (appendable.length > 0) {
7086				/**
7087				 * @summary Raised before the template appends any items to itself.
7088				 * @event FooGallery.Template~"append-items.foogallery"
7089				 * @type {jQuery.Event}
7090				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
7091				 * @param {FooGallery.Template} template - The template raising the event.
7092				 * @param {FooGallery.Item[]} items - The array of items to append.
7093				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
7094				 * $(".foogallery").foogallery({
7095				 * 	on: {
7096				 * 		"append-items.foogallery": function(event, template, items){
7097				 * 			// do something
7098				 * 		}
7099				 * 	}
7100				 * });
7101				 * @example {@caption Calling the `preventDefault` method on the `event` object will prevent any items being appended.}
7102				 * $(".foogallery").foogallery({
7103				 * 	on: {
7104				 * 		"append-items.foogallery": function(event, template, items){
7105				 * 			if ("some condition"){
7106				 * 				// stop any items being appended
7107				 * 				event.preventDefault();
7108				 * 			}
7109				 * 		}
7110				 * 	}
7111				 * });
7112				 */
7113				var e = self.tmpl.trigger("append-items", [appendable]);
7114				if (!e.isDefaultPrevented()) {
7115					appended = $.map(appendable, function (item) {
7116						return item.append() ? item : null;
7117					});
7118				}
7119				/**
7120				 * @summary Raised after the template has appended items to itself.
7121				 * @event FooGallery.Template~"appended-items.foogallery"
7122				 * @type {jQuery.Event}
7123				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
7124				 * @param {FooGallery.Template} template - The template raising the event.
7125				 * @param {FooGallery.Item[]} items - The array of items appended.
7126				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
7127				 * $(".foogallery").foogallery({
7128					 * 	on: {
7129					 * 		"appended-items.foogallery": function(event, template, items){
7130					 * 			// do something
7131					 * 		}
7132					 * 	}
7133					 * });
7134				 */
7135				if (appended.length > 0) self.tmpl.trigger("appended-items", [appended]);
7136			}
7137			return appended;
7138		},
7139		/**
7140		 * @summary Detach each of the supplied {@link FooGallery.Item|`items`} from the template.
7141		 * @memberof FooGallery.Items#
7142		 * @function detach
7143		 * @param {FooGallery.Item[]} items - The array of items to detach.
7144		 * @returns {FooGallery.Item[]} The array of items that were detached.
7145		 * @fires FooGallery.Template~"detach-items.foogallery"
7146		 * @fires FooGallery.Template~"detached-items.foogallery"
7147		 */
7148		detach: function (items) {
7149			var self = this, detached = [], detachable = self.detachable(items);
7150			if (detachable.length > 0) {
7151				/**
7152				 * @summary Raised before the template detaches any items from itself.
7153				 * @event FooGallery.Template~"detach-items.foogallery"
7154				 * @type {jQuery.Event}
7155				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
7156				 * @param {FooGallery.Template} template - The template raising the event.
7157				 * @param {FooGallery.Item[]} items - The array of items to detach.
7158				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
7159				 * $(".foogallery").foogallery({
7160				 * 	on: {
7161				 * 		"detach-items.foogallery": function(event, template, items){
7162				 * 			// do something
7163				 * 		}
7164				 * 	}
7165				 * });
7166				 * @example {@caption Calling the `preventDefault` method on the `event` object will prevent any items being detached.}
7167				 * $(".foogallery").foogallery({
7168				 * 	on: {
7169				 * 		"detach-items.foogallery": function(event, template, items){
7170				 * 			if ("some condition"){
7171				 * 				// stop any items being detached
7172				 * 				event.preventDefault();
7173				 * 			}
7174				 * 		}
7175				 * 	}
7176				 * });
7177				 */
7178				var e = self.tmpl.trigger("detach-items", [detachable]);
7179				if (!e.isDefaultPrevented()) {
7180					detached = $.map(detachable, function (item) {
7181						return item.detach() ? item : null;
7182					});
7183				}
7184				/**
7185				 * @summary Raised after the template has detached items from itself.
7186				 * @event FooGallery.Template~"detached-items.foogallery"
7187				 * @type {jQuery.Event}
7188				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
7189				 * @param {FooGallery.Template} template - The template raising the event.
7190				 * @param {FooGallery.Item[]} items - The array of items detached.
7191				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
7192				 * $(".foogallery").foogallery({
7193					 * 	on: {
7194					 * 		"detached-items.foogallery": function(event, template, items){
7195					 * 			// do something
7196					 * 		}
7197					 * 	}
7198					 * });
7199				 */
7200				if (detached.length > 0) self.tmpl.trigger("detached-items", [detached]);
7201			}
7202			return detached;
7203		},
7204		observe: function(item){
7205			var self = this;
7206			if (self.iobserver && item.isCreated && item.isAttached && (!item.isLoading || !item.isLoaded)){
7207				self.iobserver.observe(item.el);
7208				self._observed.set(item.el, item);
7209			}
7210		},
7211		unobserve: function(item){
7212			var self = this;
7213			if (self.iobserver) {
7214				self.iobserver.unobserve(item.el);
7215				self._observed.delete(item.el);
7216			}
7217		}
7218	});
7219
7220	_.components.register("items", _.Items);
7221
7222})(
7223		FooGallery.$,
7224		FooGallery,
7225		FooGallery.utils,
7226		FooGallery.utils.is,
7227		FooGallery.utils.fn,
7228		FooGallery.utils.obj
7229);
7230(function ($, _, _utils, _is, _fn, _obj, _str) {
7231
7232	_.Item = _.Component.extend(/** @lends FooGallery.Item.prototype */{
7233		/**
7234		 * @summary The base class for an item.
7235		 * @memberof FooGallery
7236		 * @constructs Item
7237		 * @param {FooGallery.Template} template - The template this item belongs to.
7238		 * @param {FooGallery.Item~Options} [options] - The options to initialize the item with.
7239		 * @augments FooGallery.Component
7240		 * @borrows FooGallery.utils.Class.extend as extend
7241		 * @borrows FooGallery.utils.Class.override as override
7242		 */
7243		construct: function (template, options) {
7244			var self = this;
7245			/**
7246			 * @ignore
7247			 * @memberof FooGallery.Item#
7248			 * @function _super
7249			 */
7250			self._super(template);
7251			self.cls = template.cls.item;
7252			self.il8n = template.il8n.item;
7253			self.sel = template.sel.item;
7254			self.opt = _obj.extend({}, template.opt.item, options);
7255
7256			/**
7257			 * @summary Whether or not the items' elements are appended to the template.
7258			 * @memberof FooGallery.Item#
7259			 * @name isAttached
7260			 * @type {boolean}
7261			 * @readonly
7262			 */
7263			self.isAttached = false;
7264			/**
7265			 * @summary Whether or not the items' elements are created and can be used.
7266			 * @memberof FooGallery.Item#
7267			 * @name isCreated
7268			 * @type {boolean}
7269			 * @readonly
7270			 */
7271			self.isCreated = false;
7272			/**
7273			 * @summary Whether or not the item has been destroyed and can not be used.
7274			 * @memberof FooGallery.Item#
7275			 * @name isDestroyed
7276			 * @type {boolean}
7277			 * @readonly
7278			 */
7279			self.isDestroyed = false;
7280			/**
7281			 * @summary Whether or not the items' image is currently loading.
7282			 * @memberof FooGallery.Item#
7283			 * @name isLoading
7284			 * @type {boolean}
7285			 * @readonly
7286			 */
7287			self.isLoading = false;
7288			/**
7289			 * @summary Whether or not the items' image has been loaded.
7290			 * @memberof FooGallery.Item#
7291			 * @name isLoaded
7292			 * @type {boolean}
7293			 * @readonly
7294			 */
7295			self.isLoaded = false;
7296			/**
7297			 * @summary Whether or not the items' image threw an error while loading.
7298			 * @memberof FooGallery.Item#
7299			 * @name isError
7300			 * @type {boolean}
7301			 * @readonly
7302			 */
7303			self.isError = false;
7304			/**
7305			 * @summary Whether or not this item was parsed from an existing DOM element.
7306			 * @memberof FooGallery.Item#
7307			 * @name isParsed
7308			 * @type {boolean}
7309			 * @readonly
7310			 */
7311			self.isParsed = false;
7312			/**
7313			 * @summary Whether or not this items image is a <picture> element.
7314			 * @memberof FooGallery.Item#
7315			 * @name isPicture
7316			 * @type {boolean}
7317			 * @readonly
7318			 */
7319			self.isPicture = false;
7320			/**
7321			 * @memberof FooGallery.Item#
7322			 * @name $el
7323			 * @type {?jQuery}
7324			 */
7325			self.$el = null;
7326			/**
7327			 * @memberof FooGallery.Item#
7328			 * @name el
7329			 * @type {?Element}
7330			 */
7331			self.el = null;
7332			/**
7333			 * @memberof FooGallery.Item#
7334			 * @name $inner
7335			 * @type {?jQuery}
7336			 */
7337			self.$inner = null;
7338			/**
7339			 * @memberof FooGallery.Item#
7340			 * @name $anchor
7341			 * @type {?jQuery}
7342			 */
7343			self.$anchor = null;
7344			/**
7345			 * @memberof FooGallery.Item#
7346			 * @name $overlay
7347			 * @type {?jQuery}
7348			 */
7349			self.$overlay = null;
7350			/**
7351			 * @memberof FooGallery.Item#
7352			 * @name $wrap
7353			 * @type {?jQuery}
7354			 */
7355			self.$wrap = null;
7356			/**
7357			 * @memberof FooGallery.Item#
7358			 * @name $image
7359			 * @type {?jQuery}
7360			 */
7361			self.$image = null;
7362			/**
7363			 * @memberof FooGallery.Item#
7364			 * @name $caption
7365			 * @type {?jQuery}
7366			 */
7367			self.$caption = null;
7368			/**
7369			 * @memberof FooGallery.Item#
7370			 * @name $loader
7371			 * @type {?jQuery}
7372			 */
7373			self.$loader = null;
7374
7375			/**
7376			 * @memberof FooGallery.Item#
7377			 * @name index
7378			 * @type {number}
7379			 * @default -1
7380			 */
7381			self.index = -1;
7382			/**
7383			 * @memberof FooGallery.Item#
7384			 * @name type
7385			 * @type {string}
7386			 */
7387			self.type = self.opt.type;
7388			/**
7389			 * @memberof FooGallery.Item#
7390			 * @name id
7391			 * @type {string}
7392			 */
7393			self.id = `${ self.opt.id }`;
7394			/**
7395			 * @memberof FooGallery.Item#
7396			 * @name productId
7397			 * @type {string}
7398			 */
7399			self.productId = self.opt.productId;
7400			/**
7401			 * @memberof FooGallery.Item#
7402			 * @name href
7403			 * @type {string}
7404			 */
7405			self.href = self.opt.href;
7406			/**
7407			 * @memberof FooGallery.Item#
7408			 * @name download
7409			 * @type {string}
7410			 */
7411			self.download = self.opt.download;
7412			/**
7413			 * @memberof FooGallery.Item#
7414			 * @name placeholder
7415			 * @type {string}
7416			 */
7417			self.placeholder = self.opt.placeholder;
7418			/**
7419			 * @memberof FooGallery.Item#
7420			 * @name src
7421			 * @type {string}
7422			 */
7423			self.src = self.opt.src;
7424			/**
7425			 * @memberof FooGallery.Item#
7426			 * @name srcset
7427			 * @type {string}
7428			 */
7429			self.srcset = self.opt.srcset;
7430			/**
7431			 * @memberof FooGallery.Item#
7432			 * @name sources
7433			 * @type {Object[]}
7434			 */
7435			self.sources = self.opt.sources;
7436			/**
7437			 * @memberof FooGallery.Item#
7438			 * @name width
7439			 * @type {number}
7440			 */
7441			self.width = self.opt.width;
7442			/**
7443			 * @memberof FooGallery.Item#
7444			 * @name height
7445			 * @type {number}
7446			 */
7447			self.height = self.opt.height;
7448			/**
7449			 * @memberof FooGallery.Item#
7450			 * @name title
7451			 * @type {string}
7452			 */
7453			self.title = self.opt.title;
7454			/**
7455			 * @memberof FooGallery.Item#
7456			 * @name alt
7457			 * @type {string}
7458			 */
7459			self.alt = self.opt.alt;
7460			/**
7461			 * @memberof FooGallery.Item#
7462			 * @name caption
7463			 * @type {string}
7464			 */
7465			self.caption = _.safeParse(self.opt.caption);
7466			/**
7467			 * @memberof FooGallery.Item#
7468			 * @name description
7469			 * @type {string}
7470			 */
7471			self.description = _.safeParse(self.opt.description);
7472			/**
7473			 * @memberof FooGallery.Item#
7474			 * @name attrItem
7475			 * @type {FooGallery.Item~Attributes}
7476			 */
7477			self.attr = self.opt.attr;
7478			var attr = self.attr || {},
7479				anchor = attr.anchor || {},
7480				image = attr.image || {},
7481				caption = anchor["data-title"] || anchor["data-caption-title"],
7482				description = anchor["data-description"] || anchor["data-caption-desc"];
7483
7484			if (_is.empty(self.title) && _is.string(image.title)) self.title = _.safeParse(image.title);
7485			if (_is.empty(self.alt) && _is.string(image.alt)) self.alt = _.safeParse(image.alt);
7486			if (_is.empty(self.caption) && _is.string(caption)) self.caption = _.safeParse(caption);
7487			if (_is.empty(self.description) && _is.string(description)) self.description = _.safeParse(description);
7488			/**
7489			 * @memberof FooGallery.Item#
7490			 * @name tags
7491			 * @type {string[]}
7492			 */
7493			self.tags = self.opt.tags;
7494			/**
7495			 * @memberof FooGallery.Item#
7496			 * @name maxCaptionLength
7497			 * @type {number}
7498			 */
7499			self.maxCaptionLength = self.opt.maxCaptionLength;
7500			/**
7501			 * @memberof FooGallery.Item#
7502			 * @name maxDescriptionLength
7503			 * @type {number}
7504			 */
7505			self.maxDescriptionLength = self.opt.maxDescriptionLength;
7506			/**
7507			 * @memberof FooGallery.Item#
7508			 * @name showCaptionTitle
7509			 * @type {boolean}
7510			 */
7511			self.showCaptionTitle = self.opt.showCaptionTitle;
7512			/**
7513			 * @memberof FooGallery.Item#
7514			 * @name showCaptionDescription
7515			 * @type {boolean}
7516			 */
7517			self.showCaptionDescription = self.opt.showCaptionDescription;
7518			self.mobileCaptionOverrides = self.parseMobileCaptionOverrides(anchor["data-mobile-captions"]);
7519			self.thumbnailCaption = self.caption;
7520			self.thumbnailDescription = self.description;
7521			self.thumbnailShowCaptionTitle = self.showCaptionTitle;
7522			self.thumbnailShowCaptionDescription = self.showCaptionDescription;
7523			/**
7524			 * @memberof FooGallery.Item#
7525			 * @name noLightbox
7526			 * @type {boolean}
7527			 */
7528			self.noLightbox = self.opt.noLightbox;
7529			/**
7530			 * @memberof FooGallery.Item#
7531			 * @name panelHide
7532			 * @type {boolean}
7533			 */
7534			self.panelHide = self.opt.panelHide;
7535			/**
7536			 * @memberof FooGallery.Item#
7537			 * @name exif
7538			 * @type {Object}
7539			 */
7540			self.exif = self.opt.exif;
7541			/**
7542			 * @memberof FooGallery.Item#
7543			 * @name hasExif
7544			 * @type {boolean}
7545			 */
7546			self.hasExif = _is.exif(self.exif);
7547			/**
7548			 * @memberof FooGallery.Item#
7549			 * @name ribbon
7550			 * @type {FooGallery.Item~Ribbon}
7551			 */
7552			self.ribbon = self.opt.ribbon;
7553			/**
7554			 * @memberof FooGallery.Item#
7555			 * @name hasRibbon
7556			 * @type {boolean}
7557			 */
7558			self.hasRibbon = _is.hash(self.ribbon) && _is.string(self.ribbon.text) && _is.string(self.ribbon.type);
7559			/**
7560			 * @memberof FooGallery.Item#
7561			 * @name buttons
7562			 * @type {FooGallery.Item~Button[]}
7563			 */
7564			self.buttons = self.opt.buttons;
7565			/**
7566			 * @summary This property is used to store the promise created when loading an item for the first time.
7567			 * @memberof FooGallery.Item#
7568			 * @name _load
7569			 * @type {?Promise}
7570			 * @private
7571			 */
7572			self._load = null;
7573			/**
7574			 * @summary This property is used to store the init state of an item the first time it is parsed and is used to reset state during destroy.
7575			 * @memberof FooGallery.Item#
7576			 * @name _undo
7577			 * @type {Object}
7578			 * @private
7579			 */
7580			self._undo = {
7581				classes: "",
7582				style: "",
7583				placeholder: false,
7584				captionExists: false,
7585				captionHTML: ""
7586			};
7587		},
7588		/**
7589		 * Parse the optional thumbnail-only mobile caption payload.
7590		 * @param {*} value Serialized or parsed mobile caption overrides.
7591		 * @returns {?Object}
7592		 */
7593		parseMobileCaptionOverrides: function (value) {
7594			if (_is.string(value)) {
7595				try {
7596					value = JSON.parse(value);
7597				} catch (e) {
7598					return null;
7599				}
7600			}
7601			if (!_is.hash(value)) return null;
7602			var overrides = {};
7603			["title", "description"].forEach(function (key) {
7604				if (Object.prototype.hasOwnProperty.call(value, key) && _is.string(value[key])) {
7605					overrides[key] = _.safeParse(value[key]);
7606				}
7607			});
7608			return Object.keys(overrides).length > 0 ? overrides : null;
7609		},
7610		/**
7611		 * Resolve the text and visibility used by the thumbnail caption.
7612		 * Canonical caption properties are deliberately left unchanged for the
7613		 * lightbox and other item consumers.
7614		 */
7615		updateThumbnailCaptionValues: function () {
7616			var self = this,
7617				overrides = self.mobileCaptionOverrides,
7618				mobile = self.tmpl && self.tmpl.opt && self.tmpl.opt.mobileCaptions;
7619			self.thumbnailCaption = self.caption;
7620			self.thumbnailDescription = self.description;
7621			self.thumbnailShowCaptionTitle = self.showCaptionTitle;
7622			self.thumbnailShowCaptionDescription = self.showCaptionDescription;
7623			if (!mobile || !_is.hash(overrides)) return;
7624			if (Object.prototype.hasOwnProperty.call(overrides, "title")) {
7625				self.thumbnailCaption = overrides.title;
7626				self.thumbnailShowCaptionTitle = self.thumbnailCaption.length > 0;
7627			}
7628			if (Object.prototype.hasOwnProperty.call(overrides, "description")) {
7629				self.thumbnailDescription = overrides.description;
7630				self.thumbnailShowCaptionDescription = self.thumbnailDescription.length > 0;
7631			}
7632		},
7633		/**
7634		 * Update parsed caption markup while retaining the original desktop DOM.
7635		 */
7636		updateParsedCaption: function () {
7637			var self = this,
7638				cls = self.cls,
7639				sel = self.sel,
7640				overrides = self.mobileCaptionOverrides,
7641				applyTitle = self.tmpl && self.tmpl.opt && self.tmpl.opt.mobileCaptions && _is.hash(overrides) && Object.prototype.hasOwnProperty.call(overrides, "title"),
7642				applyDescription = self.tmpl && self.tmpl.opt && self.tmpl.opt.mobileCaptions && _is.hash(overrides) && Object.prototype.hasOwnProperty.call(overrides, "description"),
7643				captionTitle = applyTitle && self.thumbnailShowCaptionTitle && _is.string(self.thumbnailCaption) && self.thumbnailCaption.length > 0,
7644				captionDescription = applyDescription && self.thumbnailShowCaptionDescription && _is.string(self.thumbnailDescription) && self.thumbnailDescription.length > 0;
7645
7646			if (!applyTitle && !applyDescription) return;
7647
7648			if (!self.$caption.length && (captionTitle || captionDescription)) {
7649				var caption = document.createElement("figcaption"),
7650					captionInner = document.createElement("div");
7651				caption.className = cls.caption.elem;
7652				captionInner.className = cls.caption.inner;
7653				caption.appendChild(captionInner);
7654				self.$inner.append(caption);
7655				self.$caption = $(caption).on("click.foogallery", {self: self}, self.onCaptionClick);
7656				self.$caption.on("click.foogallery", ".fg-download-button", {self: self}, self.onDownloadClick);
7657			}
7658			if (!self.$caption.length) return;
7659
7660			var innerSelector = sel.caption.inner || "." + cls.caption.inner,
7661				$inner = self.$caption.find(innerSelector).first();
7662			if (!$inner.length) {
7663				$inner = $("<div>").addClass(cls.caption.inner).appendTo(self.$caption);
7664			}
7665
7666			if (applyTitle) {
7667				var $title = self.$caption.find(sel.caption.title).first();
7668				if (captionTitle) {
7669					if (!$title.length) {
7670						$title = $("<div>").addClass(cls.caption.title);
7671						$title.prependTo($inner);
7672					}
7673					$title.html(self.maxCaptionLength > 0 ? _str.trimTo(self.thumbnailCaption, self.maxCaptionLength) : self.thumbnailCaption);
7674				} else {
7675					$title.remove();
7676				}
7677			}
7678
7679			if (applyDescription) {
7680				var $description = self.$caption.find(sel.caption.description).first();
7681				if (captionDescription) {
7682					if (!$description.length) {
7683						$description = $("<div>").addClass(cls.caption.description);
7684						var $buttons = $inner.find(sel.caption.buttons || "." + cls.caption.buttons).first();
7685						if ($buttons.length) $description.insertBefore($buttons);
7686						else $inner.append($description);
7687					}
7688					$description.html(self.maxDescriptionLength > 0 ? _str.trimTo(self.thumbnailDescription, self.maxDescriptionLength) : self.thumbnailDescription);
7689				} else {
7690					$description.remove();
7691				}
7692			}
7693		},
7694		/**
7695		 * @summary Destroy the item preparing it for garbage collection.
7696		 * @memberof FooGallery.Item#
7697		 * @function destroy
7698		 */
7699		destroy: function () {
7700			var self = this;
7701			/**
7702			 * @summary Raised when a template destroys an item.
7703			 * @event FooGallery.Template~"destroy-item.foogallery"
7704			 * @type {jQuery.Event}
7705			 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
7706			 * @param {FooGallery.Template} template - The template raising the event.
7707			 * @param {FooGallery.Item} item - The item to destroy.
7708			 * @returns {boolean} `true` if the {@link FooGallery.Item|`item`} has been successfully destroyed.
7709			 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
7710			 * $(".foogallery").foogallery({
7711			 * 	on: {
7712			 * 		"destroy-item.foogallery": function(event, template, item){
7713			 * 			// do something
7714			 * 		}
7715			 * 	}
7716			 * });
7717			 * @example {@caption Calling the `preventDefault` method on the `event` object will prevent the `item` being destroyed.}
7718			 * $(".foogallery").foogallery({
7719			 * 	on: {
7720			 * 		"destroy-item.foogallery": function(event, template, item){
7721			 * 			if ("some condition"){
7722			 * 				// stop the item being destroyed
7723			 * 				event.preventDefault();
7724			 * 			}
7725			 * 		}
7726			 * 	}
7727			 * });
7728			 * @example {@caption You can also prevent the default logic and replace it with your own by calling the `preventDefault` method on the `event` object.}
7729			 * $(".foogallery").foogallery({
7730			 * 	on: {
7731			 * 		"destroy-item.foogallery": function(event, template, item){
7732			 * 			// stop the default logic
7733			 * 			event.preventDefault();
7734			 * 			// replacing it with your own destroying the item yourself
7735			 * 			item.$el.off(".foogallery").remove();
7736			 * 			item.$el = null;
7737			 * 			...
7738			 * 			// once all destroy work is complete you must set isDestroyed to true
7739			 * 			item.isDestroyed = true;
7740			 * 		}
7741			 * 	}
7742			 * });
7743			 */
7744			var e = self.tmpl.trigger("destroy-item", [self]);
7745			if (!e.isDefaultPrevented()) {
7746				self.isDestroyed = self.doDestroyItem();
7747			}
7748			if (self.isDestroyed) {
7749				/**
7750				 * @summary Raised after an item has been destroyed.
7751				 * @event FooGallery.Template~"destroyed-item.foogallery"
7752				 * @type {jQuery.Event}
7753				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
7754				 * @param {FooGallery.Template} template - The template raising the event.
7755				 * @param {FooGallery.Item} item - The item that was destroyed.
7756				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
7757				 * $(".foogallery").foogallery({
7758					 * 	on: {
7759					 * 		"destroyed-item.foogallery": function(event, template, item){
7760					 * 			// do something
7761					 * 		}
7762					 * 	}
7763					 * });
7764				 */
7765				self.tmpl.trigger("destroyed-item", [self]);
7766				// call the original method that simply nulls the tmpl property
7767				self._super();
7768			}
7769			return self.isDestroyed;
7770		},
7771		/**
7772		 * @summary Performs the actual destroy logic for the item.
7773		 * @memberof FooGallery.Item#
7774		 * @function doDestroyItem
7775		 * @returns {boolean}
7776		 */
7777		doDestroyItem: function () {
7778			var self = this;
7779			if (self.isParsed) {
7780				self.$anchor.add(self.$caption).off("click.foogallery");
7781				if (self._undo.captionExists) self.$caption.html(self._undo.captionHTML);
7782				else self.$caption.remove();
7783				self.append();
7784
7785				self.tmpl.items.unobserve(self);
7786
7787				if (_is.empty(self._undo.classes)) self.$el.removeAttr("class");
7788				else self.$el.attr("class", self._undo.classes);
7789
7790				if (_is.empty(self._undo.style)) self.$el.removeAttr("style");
7791				else self.$el.attr("style", self._undo.style);
7792
7793				if (self._undo.placeholder && self.$image.prop("src") === self.placeholder) {
7794					self.$image.removeAttr("src");
7795				}
7796			} else if (self.isCreated) {
7797				self.detach();
7798				self.$el.remove();
7799			}
7800			return true;
7801		},
7802		/**
7803		 * @summary Parse the supplied element updating the current items' properties.
7804		 * @memberof FooGallery.Item#
7805		 * @function parse
7806		 * @param {(jQuery|HTMLElement|string)} element - The element to parse.
7807		 * @returns {boolean}
7808		 * @fires FooGallery.Template~"parse-item.foogallery"
7809		 * @fires FooGallery.Template~"parsed-item.foogallery"
7810		 */
7811		parse: function (element) {
7812			var self = this, $el = $(element);
7813			/**
7814			 * @summary Raised when an item needs to parse properties from an element.
7815			 * @event FooGallery.Template~"parse-item.foogallery"
7816			 * @type {jQuery.Event}
7817			 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
7818			 * @param {FooGallery.Template} template - The template raising the event.
7819			 * @param {FooGallery.Item} item - The item to populate.
7820			 * @param {jQuery} $element - The jQuery object of the element to parse.
7821			 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
7822			 * $(".foogallery").foogallery({
7823			 * 	on: {
7824			 * 		"parse-item.foogallery": function(event, template, item, $element){
7825			 * 			// do something
7826			 * 		}
7827			 * 	}
7828			 * });
7829			 * @example {@caption Calling the `preventDefault` method on the `event` object will prevent the `item` properties being parsed from the `element`.}
7830			 * $(".foogallery").foogallery({
7831			 * 	on: {
7832			 * 		"parse-item.foogallery": function(event, template, item, $element){
7833			 * 			if ("some condition"){
7834			 * 				// stop the item being parsed
7835			 * 				event.preventDefault();
7836			 * 			}
7837			 * 		}
7838			 * 	}
7839			 * });
7840			 * @example {@caption You can also prevent the default logic and replace it with your own by calling the `preventDefault` method on the `event` object and then populating the `item` properties from the `element`.}
7841			 * $(".foogallery").foogallery({
7842			 * 	on: {
7843			 * 		"parse-item.foogallery": function(event, template, item, $element){
7844			 * 			// stop the default logic
7845			 * 			event.preventDefault();
7846			 * 			// replacing it with your own setting each property of the item yourself
7847			 * 			item.$el = $element;
7848			 * 			...
7849			 * 			// once all properties are set you must set isParsed to true
7850			 * 			item.isParsed = true;
7851			 * 		}
7852			 * 	}
7853			 * });
7854			 */
7855			var e = self.tmpl.trigger("parse-item", [self, $el]);
7856			if (!e.isDefaultPrevented() && (self.isCreated = $el.is(self.sel.elem))) {
7857				self.isParsed = self.doParseItem($el);
7858				// We don't load the attributes when parsing as they are only ever used to create an item and if you're parsing it's already created.
7859			}
7860			if (self.isParsed) {
7861				if (!self.isLoaded) self.tmpl.items.observe(self);
7862				/**
7863				 * @summary Raised after an item has been parsed from an element.
7864				 * @event FooGallery.Template~"parsed-item.foogallery"
7865				 * @type {jQuery.Event}
7866				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
7867				 * @param {FooGallery.Template} template - The template raising the event.
7868				 * @param {FooGallery.Item} item - The item that was parsed.
7869				 * @param {jQuery} $element - The jQuery object of the element that was parsed.
7870				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
7871				 * $(".foogallery").foogallery({
7872				 * 	on: {
7873				 * 		"parsed-item.foogallery": function(event, template, item, $element){
7874				 * 			// do something
7875				 * 		}
7876				 * 	}
7877				 * });
7878				 */
7879				self.tmpl.trigger("parsed-item", [self]);
7880			}
7881			return self.isParsed;
7882		},
7883		/**
7884		 * @summary Performs the actual parse logic for the item.
7885		 * @memberof FooGallery.Item#
7886		 * @function doParseItem
7887		 * @param {jQuery} $el - The jQuery element to parse.
7888		 * @returns {boolean}
7889		 */
7890		doParseItem: function ($el) {
7891			var self = this,
7892				cls = self.cls,
7893				sel = self.sel,
7894				el = $el.get(0);
7895
7896			self._undo.classes = $el.attr("class") || "";
7897			self._undo.style = $el.attr("style") || "";
7898
7899			self.$el = $el.data(_.DATA_ITEM, self);
7900			self.el = el;
7901			self.$inner = $(el.querySelector(sel.inner));
7902			self.$anchor = $(el.querySelector(sel.anchor)).on("click.foogallery", {self: self}, self.onAnchorClick);
7903			self.$image = $(el.querySelector(sel.image));
7904			self.$caption = $(el.querySelector(sel.caption.elem)).on("click.foogallery", {self: self}, self.onCaptionClick);
7905			self._undo.captionExists = self.$caption.length > 0;
7906			self._undo.captionHTML = self.$caption.length ? self.$caption.html() : "";
7907			self.$caption.on("click.foogallery", ".fg-download-button", {self: self}, self.onDownloadClick);
7908			self.$overlay = $(el.querySelector(sel.overlay));
7909			self.$wrap = $(el.querySelector(sel.wrap));
7910			self.$loader = $(el.querySelector(sel.loader));
7911
7912			if ( !self.$el.length || !self.$inner.length || !self.$anchor.length || !self.$image.length ){
7913				console.error("FooGallery Error: Invalid HTML markup. Check the item markup for additional elements or malformed HTML in the title or description.", self);
7914				self.isError = true;
7915				self.tmpl.trigger("error-item", [self]);
7916				if (self.$el.length !== 0){
7917					self.$el.remove();
7918				}
7919				return false;
7920			}
7921
7922			self.isAttached = el.parentNode !== null;
7923			self.isLoading = self.$el.hasClass(cls.loading);
7924			self.isLoaded = self.$el.hasClass(cls.loaded);
7925			self.isError = self.$el.hasClass(cls.error);
7926
7927			var data = self.$anchor.data();
7928			self.mobileCaptionOverrides = self.parseMobileCaptionOverrides(data.mobileCaptions || self.$anchor.attr("data-mobile-captions"));
7929			self.id = `${ data.id || data.attachmentId || self.id }`;
7930			self.productId = data.productId || self.productId;
7931			self.tags = data.tags || self.tags;
7932			self.href = data.href || self.$anchor.attr('href') || self.href;
7933			self.download = data.download || self.download || self.href;
7934
7935			var $img;
7936			if (self.$image.is("picture")){
7937				self.isPicture = true;
7938				self.sources = self.$image.find("source").map(function(i, source){
7939					return {
7940						srcset: source.getAttribute(self.tmpl.opt.srcset),
7941						type: source.getAttribute("type"),
7942						media: source.getAttribute("media"),
7943						sizes: source.getAttribute("sizes")
7944					};
7945				}).get();
7946				$img = self.$image.find("img");
7947			} else {
7948				$img = self.$image;
7949			}
7950
7951			self.src = $img.attr(self.tmpl.opt.src) || self.src;
7952			self.srcset = $img.attr(self.tmpl.opt.srcset) || self.srcset;
7953			self.width = parseInt($img.attr("width")) || self.width;
7954			self.height = parseInt($img.attr("height")) || self.height;
7955			self.title = _.safeParse( $img.attr("title") || self.title );
7956			self.alt = _.safeParse( $img.attr("alt") || self.alt );
7957
7958			self.caption = _.safeParse( data.title || data.captionTitle || self.caption );
7959			self.description = _.safeParse( data.description || data.captionDesc || self.description );
7960			self.updateThumbnailCaptionValues();
7961			self.noLightbox = self.$anchor.hasClass(cls.noLightbox);
7962			self.panelHide = self.$anchor.hasClass(cls.panelHide);
7963			if (_is.exif(data.exif)){
7964				self.exif = _obj.extend(self.exif, data.exif);
7965				self.hasExif = true;
7966			}
7967			// enforce the max lengths for the caption and description
7968			if (self.maxCaptionLength > 0){
7969				var title = _str.trimTo(self.caption, self.maxCaptionLength);
7970				if (title !== self.caption) {
7971					self.$caption.find(sel.caption.title).html(title);
7972				}
7973			}
7974			if (self.maxDescriptionLength > 0){
7975				var desc = _str.trimTo(self.description, self.maxDescriptionLength);
7976				if (desc !== self.description) {
7977					self.$caption.find(sel.caption.description).html(desc);
7978				}
7979			}
7980			if (!self.showCaptionTitle) {
7981				self.$caption.find(sel.caption.title).remove();
7982			}
7983			if (!self.showCaptionDescription) {
7984				self.$caption.find(sel.caption.description).remove();
7985			}
7986			self.updateParsedCaption();
7987
7988			// if the image has no src url then set the placeholder
7989			var img = $img.get(0);
7990			if (!_is.string(img.src) || img.src.length === 0) {
7991				if (!_is.string(self.placeholder) || self.placeholder.length === 0){
7992					self.placeholder = self.createPlaceholder(self.width, self.height);
7993				}
7994				if (self.placeholder.length > 0){
7995					img.src = self.placeholder;
7996					self._undo.placeholder = true;
7997				}
7998			}
7999
8000			var typeClass = self.getTypeClass();
8001			if (!self.$el.hasClass(typeClass)){
8002				self.$el.addClass(typeClass);
8003			}
8004			if (self.hasExif && !self.$el.hasClass(cls.exif)){
8005				self.$el.addClass(cls.exif);
8006			}
8007			if (self.isCreated && self.isAttached && !self.isLoading && !self.isLoaded && !self.isError && !self.$el.hasClass(cls.idle)) {
8008				self.$el.addClass(cls.idle);
8009			}
8010
8011			self.doShortPixel();
8012
8013			return true;
8014		},
8015		/**
8016		 * @summary Create the items' DOM elements and populate the corresponding properties.
8017		 * @memberof FooGallery.Item#
8018		 * @function create
8019		 * @returns {boolean}
8020		 * @fires FooGallery.Template~"create-item.foogallery"
8021		 * @fires FooGallery.Template~"created-item.foogallery"
8022		 */
8023		create: function () {
8024			var self = this;
8025			if (!self.isCreated && _is.string(self.href) && _is.string(self.src) && _is.number(self.width) && _is.number(self.height)) {
8026				/**
8027				 * @summary Raised when an item needs to create its' elements.
8028				 * @event FooGallery.Template~"create-item.foogallery"
8029				 * @type {jQuery.Event}
8030				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
8031				 * @param {FooGallery.Template} template - The template raising the event.
8032				 * @param {FooGallery.Item} item - The item to create the elements for.
8033				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
8034				 * $(".foogallery").foogallery({
8035				 * 	on: {
8036				 * 		"create-item.foogallery": function(event, template, item){
8037				 * 			// do something
8038				 * 		}
8039				 * 	}
8040				 * });
8041				 * @example {@caption Calling the `preventDefault` method on the `event` object will prevent the `item` being created.}
8042				 * $(".foogallery").foogallery({
8043				 * 	on: {
8044				 * 		"create-item.foogallery": function(event, template, item){
8045				 * 			if ("some condition"){
8046				 * 				// stop the item being created
8047				 * 				event.preventDefault();
8048				 * 			}
8049				 * 		}
8050				 * 	}
8051				 * });
8052				 * @example {@caption You can also prevent the default logic and replace it with your own by calling the `preventDefault` method on the `event` object.}
8053				 * $(".foogallery").foogallery({
8054				 * 	on: {
8055				 * 		"create-item.foogallery": function(event, template, item){
8056				 * 			// stop the default logic
8057				 * 			event.preventDefault();
8058				 * 			// replacing it with your own creating each element property of the item yourself
8059				 * 			item.$el = $("<div/>");
8060				 * 			...
8061				 * 			// once all elements are created you must set isCreated to true
8062				 * 			item.isCreated = true;
8063				 * 		}
8064				 * 	}
8065				 * });
8066				 */
8067				var e = self.tmpl.trigger("create-item", [self]);
8068				if (!e.isDefaultPrevented()) {
8069					self.isCreated = self.doCreateItem();
8070				}
8071				if (self.isCreated) {
8072					/**
8073					 * @summary Raised after an items' elements have been created.
8074					 * @event FooGallery.Template~"created-item.foogallery"
8075					 * @type {jQuery.Event}
8076					 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
8077					 * @param {FooGallery.Template} template - The template raising the event.
8078					 * @param {FooGallery.Item} item - The item that was created.
8079					 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
8080					 * $(".foogallery").foogallery({
8081					 * 	on: {
8082					 * 		"created-item.foogallery": function(event, template, item){
8083					 * 			// do something
8084					 * 		}
8085					 * 	}
8086					 * });
8087					 */
8088					self.tmpl.trigger("created-item", [self]);
8089				}
8090			}
8091			return self.isCreated;
8092		},
8093		/**
8094		 * @memberof FooGallery.Item#
8095		 * @function _setAttributes
8096		 * @param element
8097		 * @param attributes
8098		 * @private
8099		 */
8100		_setAttributes: function(element, attributes){
8101			Object.entries( attributes ).forEach( ( [ key, value ] ) => {
8102				if ( !_is.empty( value ) ) {
8103					if ( key === "class" ) {
8104						const classes = ( _is.array( value ) ? value : [ value ] )
8105							.flatMap( className => _is.string( className ) ? className.split( " " ) : [] )
8106							.map( p => p.trim() )
8107							.filter( Boolean );
8108
8109						element.classList.add( ...classes );
8110					} else {
8111						element.setAttribute( key, _is.string( value ) ? value : JSON.stringify( value ) );
8112					}
8113				}
8114			} );
8115		},
8116		/**
8117		 * @summary Performs some checks for ShortPixel integration and WebP support.
8118		 * @memberof FooGallery.Item#
8119		 * @function doShortPixel
8120		 */
8121		doShortPixel: function(){
8122			var self = this;
8123			if (self.tmpl.opt.shortpixel && !_.supportsWebP){
8124				var regex = /([\/,+])to_webp([\/,+])/i;
8125				function spReplacer(match, $1, $2){
8126					return $1 === "/" || $2 === "/" ? "/" : $1;
8127				}
8128				self.href = self.href.replace(regex, spReplacer);
8129				self.src = self.src.replace(regex, spReplacer);
8130				self.srcset = self.srcset.replace(regex, spReplacer);
8131			}
8132		},
8133		/**
8134		 * @summary Performs the actual create logic for the item.
8135		 * @memberof FooGallery.Item#
8136		 * @function doCreateItem
8137		 * @returns {boolean}
8138		 */
8139		doCreateItem: function () {
8140			var self = this,
8141				cls = self.cls,
8142				attr = self.attr,
8143				exif = self.hasExif ? cls.exif : "";
8144
8145			self.updateThumbnailCaptionValues();
8146
8147			self.isLoaded = !self.tmpl.opt.lazy;
8148			self.isPicture = self.sources.length > 0;
8149
8150			self.doShortPixel();
8151
8152			var elem = document.createElement("div");
8153			self._setAttributes(elem, attr.elem);
8154			self._setAttributes(elem, {
8155				"class": [cls.elem, self.getTypeClass(), exif, self.isLoaded ? cls.loaded : cls.idle]
8156			});
8157
8158			var inner = document.createElement("figure");
8159			self._setAttributes(inner, attr.inner);
8160			self._setAttributes(inner, {
8161				"class": cls.inner
8162			});
8163
8164			var anchorClasses = [cls.anchor];
8165			if (self.noLightbox){
8166				anchorClasses.push(cls.noLightbox);
8167			}
8168			if (self.panelHide){
8169				anchorClasses.push(cls.panelHide);
8170			}
8171
8172			var anchor = document.createElement("a");
8173			self._setAttributes(anchor, attr.anchor);
8174			self._setAttributes(anchor, {
8175				"class": anchorClasses.join(" "),
8176				"href": self.href,
8177				"data-id": self.id,
8178				"data-type": self.type,
8179				"data-title": self.caption,
8180				"data-description": self.description,
8181				"data-tags": self.tags,
8182				"data-exif": self.exif,
8183				"data-product-id": self.productId,
8184				"data-download": self.download
8185			});
8186
8187			if (!_is.string(self.placeholder) || self.placeholder.length === 0){
8188				self.placeholder = self.createPlaceholder(self.width, self.height);
8189			}
8190
8191			var image = document.createElement("img");
8192			self._setAttributes(image, attr.image);
8193			var imageAttrs = {
8194				"class": cls.image,
8195				"src": self.placeholder,
8196				"width": self.width + "",
8197				"height": self.height + "",
8198				"title": self.title,
8199				"alt": self.alt
8200			};
8201			if (self.isLoaded){
8202				imageAttrs["src"] = self.src;
8203				imageAttrs["srcset"] = self.srcset;
8204			} else {
8205				imageAttrs[self.tmpl.opt.src] = self.src;
8206				imageAttrs[self.tmpl.opt.srcset] = self.srcset;
8207			}
8208			self._setAttributes(image, imageAttrs);
8209
8210			var picture;
8211			if (self.isPicture){
8212				picture = document.createElement("picture");
8213				self._setAttributes(picture, attr.picture);
8214				self.sources.forEach(function(opt){
8215					var source = document.createElement("source"),
8216						sourceAttrs = {
8217							media: opt.media,
8218							sizes: opt.sizes,
8219							type: opt.type
8220						};
8221					if (self.isLoaded){
8222						sourceAttrs["srcset"] = opt.srcset;
8223					} else {
8224						sourceAttrs[self.tmpl.opt.srcset] = opt.srcset;
8225					}
8226
8227					self._setAttributes(source, sourceAttrs);
8228					picture.appendChild(source);
8229				});
8230				picture.appendChild(image);
8231			}
8232
8233			var ribbon;
8234			if (self.hasRibbon){
8235				ribbon = document.createElement("div");
8236				ribbon.className = self.ribbon.type;
8237				var ribbonText = document.createElement("span");
8238				ribbonText.innerHTML = _.safeParse(self.ribbon.text);
8239				ribbon.appendChild(ribbonText);
8240			}
8241
8242			var overlay = document.createElement("span");
8243			overlay.className = cls.overlay;
8244
8245			var wrap = document.createElement("span");
8246			wrap.className = cls.wrap;
8247
8248			var loader = document.createElement("div");
8249			loader.className = cls.loader;
8250
8251			var caption = document.createElement("figcaption");
8252			self._setAttributes(caption, attr.caption.elem);
8253			self._setAttributes(caption, {
8254				"class": cls.caption.elem
8255			});
8256
8257			var captionInner = document.createElement("div");
8258			self._setAttributes(captionInner, attr.caption.inner);
8259			self._setAttributes(captionInner, {
8260				"class": cls.caption.inner
8261			});
8262
8263			var captionTitle = null, hasTitle = self.thumbnailShowCaptionTitle && _is.string(self.thumbnailCaption) && self.thumbnailCaption.length > 0;
8264			if (hasTitle) {
8265				captionTitle = document.createElement("div");
8266				self._setAttributes(captionTitle, attr.caption.title);
8267				captionTitle.className = cls.caption.title;
8268				captionTitle.innerHTML = self.maxCaptionLength > 0 ? _str.trimTo(self.thumbnailCaption, self.maxCaptionLength) : self.thumbnailCaption;
8269				captionInner.appendChild(captionTitle);
8270			}
8271			var captionDesc = null, hasDescription = self.thumbnailShowCaptionDescription && _is.string(self.thumbnailDescription) && self.thumbnailDescription.length > 0;
8272			if (hasDescription) {
8273				captionDesc = document.createElement("div");
8274				self._setAttributes(captionDesc, attr.caption.description);
8275				captionDesc.className = cls.caption.description;
8276				captionDesc.innerHTML = self.maxDescriptionLength > 0 ? _str.trimTo(self.thumbnailDescription, self.maxDescriptionLength) : self.thumbnailDescription;
8277				captionInner.appendChild(captionDesc);
8278			}
8279			var captionButtons = null, hasButtons = _is.array(self.buttons) && self.buttons.length > 0;
8280			if (hasButtons){
8281				captionButtons = document.createElement("div");
8282				captionButtons.className = cls.caption.buttons;
8283				_utils.each(self.buttons, function(button){
8284					if (_is.hash(button) && _is.string(button.text)){
8285						var captionButton = document.createElement("a");
8286						captionButton.innerHTML = _.safeParse(button.text);
8287						if (_is.string(button.url) && button.url.length > 0){
8288							captionButton.href = button.url;
8289						}
8290						if (_is.string(button.rel) && button.rel.length > 0){
8291							captionButton.rel = button.rel;
8292						}
8293						if (_is.string(button.target) && button.target.length > 0){
8294							captionButton.target = button.target;
8295						}
8296						const buttonClasses = [ button.classes, button.class ]
8297							.filter( classNames => _is.string( classNames ) && classNames.length > 0 )
8298							.join( " " );
8299						if ( buttonClasses.length > 0 ){
8300							captionButton.className = buttonClasses;
8301						}
8302						if (_is.hash(button.attr)){
8303							self._setAttributes(captionButton, button.attr);
8304						}
8305						// Validate the complete button after custom attributes have been applied.
8306						captionButtons.insertAdjacentHTML("beforeend", _.safeParse(captionButton.outerHTML));
8307					}
8308				});
8309				captionInner.appendChild(captionButtons);
8310			}
8311			caption.appendChild(captionInner);
8312
8313			if (self.isPicture){
8314				wrap.appendChild(picture);
8315			} else {
8316				wrap.appendChild(image);
8317			}
8318			anchor.appendChild(overlay);
8319			anchor.appendChild(wrap);
8320			inner.appendChild(anchor);
8321			if (hasTitle || hasDescription || hasButtons){
8322				inner.appendChild(caption);
8323			}
8324			if (self.hasRibbon){
8325				elem.appendChild(ribbon);
8326			}
8327			elem.appendChild(inner);
8328			elem.appendChild(loader);
8329
8330			self.$el = $(elem).data(_.DATA_ITEM, self);
8331			self.el = elem;
8332			self.$inner = $(inner);
8333			self.$anchor = $(anchor).on("click.foogallery", {self: self}, self.onAnchorClick);
8334			self.$overlay = $(overlay);
8335			self.$wrap = $(wrap);
8336			if (self.isPicture) {
8337				self.$image = $(picture);
8338			} else {
8339				self.$image = $(image);
8340			}
8341			self.$caption = $(caption).on("click.foogallery", {self: self}, self.onCaptionClick);
8342			self.$caption.on("click.foogallery", ".fg-download-button", {self: self}, self.onDownloadClick);
8343			self.$loader = $(loader);
8344
8345			return true;
8346		},
8347		/**
8348		 * @summary Append the item to the current template.
8349		 * @memberof FooGallery.Item#
8350		 * @function append
8351		 * @returns {boolean}
8352		 * @fires FooGallery.Template~"append-item.foogallery"
8353		 * @fires FooGallery.Template~"appended-item.foogallery"
8354		 */
8355		append: function () {
8356			var self = this;
8357			if (self.isCreated && !self.isAttached) {
8358				/**
8359				 * @summary Raised when an item needs to append its elements to the template.
8360				 * @event FooGallery.Template~"append-item.foogallery"
8361				 * @type {jQuery.Event}
8362				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
8363				 * @param {FooGallery.Template} template - The template raising the event.
8364				 * @param {FooGallery.Item} item - The item to append to the template.
8365				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
8366				 * $(".foogallery").foogallery({
8367				 * 	on: {
8368				 * 		"append-item.foogallery": function(event, template, item){
8369				 * 			// do something
8370				 * 		}
8371				 * 	}
8372				 * });
8373				 * @example {@caption Calling the `preventDefault` method on the `event` object will prevent the `item` being appended.}
8374				 * $(".foogallery").foogallery({
8375				 * 	on: {
8376				 * 		"append-item.foogallery": function(event, template, item){
8377				 * 			if ("some condition"){
8378				 * 				// stop the item being appended
8379				 * 				event.preventDefault();
8380				 * 			}
8381				 * 		}
8382				 * 	}
8383				 * });
8384				 * @example {@caption You can also prevent the default logic and replace it with your own by calling the `preventDefault` method on the `event` object.}
8385				 * $(".foogallery").foogallery({
8386				 * 	on: {
8387				 * 		"append-item.foogallery": function(event, template, item){
8388				 * 			// stop the default logic
8389				 * 			event.preventDefault();
8390				 * 			// replacing it with your own appending the item to the template
8391				 * 			item.$el.appendTo(template.$el);
8392				 * 			...
8393				 * 			// once the item is appended you must set isAttached to true
8394				 * 			item.isAttached = true;
8395				 * 		}
8396				 * 	}
8397				 * });
8398				 */
8399				var e = self.tmpl.trigger("append-item", [self]);
8400				if (!e.isDefaultPrevented()) {
8401					self.tmpl.$el.append(self.$el.removeClass(self.cls.hidden));
8402					self.isAttached = true;
8403				}
8404				if (self.isAttached) {
8405					if (!self.isLoaded) self.tmpl.items.observe(self);
8406					/**
8407					 * @summary Raised after an item has appended its' elements to the template.
8408					 * @event FooGallery.Template~"appended-item.foogallery"
8409					 * @type {jQuery.Event}
8410					 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
8411					 * @param {FooGallery.Template} template - The template raising the event.
8412					 * @param {FooGallery.Item} item - The item that was appended.
8413					 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
8414					 * $(".foogallery").foogallery({
8415					 * 	on: {
8416					 * 		"appended-item.foogallery": function(event, template, item){
8417					 * 			// do something
8418					 * 		}
8419					 * 	}
8420					 * });
8421					 */
8422					self.tmpl.trigger("appended-item", [self]);
8423				}
8424			}
8425			return self.isAttached;
8426		},
8427		/**
8428		 * @summary Detach the item from the current template preserving its' data and events.
8429		 * @memberof FooGallery.Item#
8430		 * @function detach
8431		 * @returns {boolean}
8432		 */
8433		detach: function () {
8434			var self = this;
8435			if (self.isCreated && self.isAttached) {
8436				self.tmpl.items.unobserve(self);
8437				/**
8438				 * @summary Raised when an item needs to detach its' elements from the template.
8439				 * @event FooGallery.Template~"detach-item.foogallery"
8440				 * @type {jQuery.Event}
8441				 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
8442				 * @param {FooGallery.Template} template - The template raising the event.
8443				 * @param {FooGallery.Item} item - The item to detach from the template.
8444				 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
8445				 * $(".foogallery").foogallery({
8446				 * 	on: {
8447				 * 		"detach-item.foogallery": function(event, template, item){
8448				 * 			// do something
8449				 * 		}
8450				 * 	}
8451				 * });
8452				 * @example {@caption Calling the `preventDefault` method on the `event` object will prevent the `item` being detached.}
8453				 * $(".foogallery").foogallery({
8454				 * 	on: {
8455				 * 		"detach-item.foogallery": function(event, template, item){
8456				 * 			if ("some condition"){
8457				 * 				// stop the item being detached
8458				 * 				event.preventDefault();
8459				 * 			}
8460				 * 		}
8461				 * 	}
8462				 * });
8463				 * @example {@caption You can also prevent the default logic and replace it with your own by calling the `preventDefault` method on the `event` object.}
8464				 * $(".foogallery").foogallery({
8465				 * 	on: {
8466				 * 		"detach-item.foogallery": function(event, template, item){
8467				 * 			// stop the default logic
8468				 * 			event.preventDefault();
8469				 * 			// replacing it with your own detaching the item from the template
8470				 * 			item.$el.detach();
8471				 * 			...
8472				 * 			// once the item is detached you must set isAttached to false
8473				 * 			item.isAttached = false;
8474				 * 		}
8475				 * 	}
8476				 * });
8477				 */
8478				var e = self.tmpl.trigger("detach-item", [self]);
8479				if (!e.isDefaultPrevented()) {
8480					self.$el.detach().removeClass(self.cls.hidden);
8481					self.isAttached = false;
8482				}
8483				if (!self.isAttached) {
8484					/**
8485					 * @summary Raised after an item has detached its' elements from the template.
8486					 * @event FooGallery.Template~"detached-item.foogallery"
8487					 * @type {jQuery.Event}
8488					 * @param {jQuery.Event} event - The jQuery.Event object for the current event.
8489					 * @param {FooGallery.Template} template - The template raising the event.
8490					 * @param {FooGallery.Item} item - The item that was detached.
8491					 * @example {@caption To listen for this event and perform some action when it occurs you would bind to it as follows.}
8492					 * $(".foogallery").foogallery({
8493					 * 	on: {
8494					 * 		"detached-item.foogallery": function(event, template, item){
8495					 * 			// do something
8496					 * 		}
8497					 * 	}
8498					 * });
8499					 */
8500					self.tmpl.trigger("detached-item", [self]);
8501				}
8502			}
8503			return !self.isAttached;
8504		},
8505		/**
8506		 * @summary Load the items' {@link FooGallery.Item#$image|$image}.
8507		 * @memberof FooGallery.Item#
8508		 * @function load
8509		 * @returns {Promise.<FooGallery.Item>}
8510		 */
8511		load: function () {
8512			var self = this;
8513			if (_is.promise(self._load)) return self._load;
8514			return self._load = $.Deferred(function (def) {
8515				if (self.isLoaded){
8516					return def.resolve(self);
8517				}
8518				if (!self.isCreated || !self.isAttached) {
8519					return def.reject("not created or attached");
8520				}
8521				var e = self.tmpl.trigger("load-item", [self]);
8522				if (e.isDefaultPrevented()){
8523					return def.reject("default prevented");
8524				}
8525
8526				self.isLoading = true;
8527				self.tmpl.items.unobserve(self);
8528				self.$el.removeClass(self.cls.idle)
8529					.removeClass(self.cls.hidden)
8530					.removeClass(self.cls.loaded)
8531					.removeClass(self.cls.error)
8532					.addClass(self.cls.loading);
8533
8534				self.loadIMG().then(function(){
8535					self.isLoading = false;
8536					self.isLoaded = true;
8537					self.$el.removeClass(self.cls.loading).addClass(self.cls.loaded);
8538					self.tmpl.trigger("loaded-item", [self]);
8539					def.resolve(self);
8540				}, function(reason){
8541					self.isLoading = false;
8542					self.isError = true;
8543					self.$el.removeClass(self.cls.loading).addClass(self.cls.error);
8544					self.tmpl.trigger("error-item", [self]);
8545					def.reject(reason);
8546				});
8547
8548			}).promise();
8549		},
8550		/**
8551		 * @summary Load the items' {@link FooGallery.Item#$image|$image} if it is an actual <img> element.
8552		 * @memberof FooGallery.Item#
8553		 * @function loadIMG
8554		 * @returns {Promise.<FooGallery.Item>}
8555		 */
8556		loadIMG: function(){
8557			var self = this;
8558			return new $.Deferred(function(def){
8559				var img = self.getImageElement();
8560				if (!img){
8561					return def.reject("Unable to find img element.");
8562				}
8563				var ph_src = img.src, ph_srcset = img.srcset;
8564				img.onload = function () {
8565					img.onload = img.onerror = null;
8566					def.resolve(img);
8567				};
8568				img.onerror = function () {
8569					img.onload = img.onerror = null;
8570					if (!_is.empty(ph_src)) {
8571						img.src = ph_src;
8572					} else {
8573						img.removeAttribute("src");
8574					}
8575					if (!_is.empty(ph_srcset)) {
8576						img.srcset = ph_srcset;
8577					} else {
8578						img.removeAttribute("srcset");
8579					}
8580					def.reject(img);
8581				};
8582				// set everything in motion by setting the src & srcset
8583				if (self.isPicture){
8584					self.$image.find("source").each(function(i, source){
8585						var srcset = source.getAttribute(self.tmpl.opt.srcset);
8586						if (!_is.empty(srcset)){
8587							source.srcset = srcset;
8588						}
8589					});
8590				}
8591
8592                const { opt: { cors } } = self.tmpl;
8593                if ( _is.string( cors ) ) {
8594                    if ( img.crossOrigin === null && _.isCrossOrigin(self.src) ) {
8595                        img.crossOrigin = cors;
8596                    }
8597				}
8598                if (!_is.empty(self.srcset)){
8599                    img.srcset = self.srcset;
8600                }
8601				img.src = self.src;
8602				if (img.complete){
8603					img.onload();
8604				}
8605			}).promise();
8606		},
8607        /**
8608         * @summary Utility method for getting the current items' <img/> element.
8609         * @returns {HTMLImageElement|undefined}
8610         */
8611		getImageElement: function(){
8612			var self = this;
8613			return self.isPicture ? self.$image.find("img").get(0) : self.$image.get(0);
8614		},
8615		/**
8616		 * @summary Create an empty placeholder image using the supplied dimensions.
8617		 * @memberof FooGallery.Item#
8618		 * @function createPlaceholder
8619		 * @param {number} width - The width of the placeholder.
8620		 * @param {number} height - The height of the placeholder.
8621		 * @returns {string}
8622		 */
8623		createPlaceholder: function(width, height){
8624			if (_is.number(width) && _is.number(height)){
8625				return "data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%22" + width + "%22%20height=%22" + height + "%22%20viewBox=%220%200%20" + width + "%20" + height + "%22%3E%3C/svg%3E";
8626			}
8627			return "";
8628		},
8629		/**
8630		 * @summary Gets the type specific CSS class for the item.
8631		 * @memberof FooGallery.Item#
8632		 * @function getTypeClass
8633		 * @returns {string}
8634		 */
8635		getTypeClass: function(){
8636			return this.cls.types[this.type] || "";
8637		},
8638		/**
8639		 * @summary Scroll the item into the center of the viewport.
8640		 * @memberof FooGallery.Item#
8641		 * @function scrollTo
8642		 */
8643		scrollTo: function (align) {
8644			var self = this;
8645			if (self.isAttached) {
8646				var el = self.$el.get(0);
8647				if (!!el.scrollIntoViewIfNeeded){
8648					el.scrollIntoViewIfNeeded();
8649				} else {
8650					el.scrollIntoView(align === "top");
8651				}
8652			}
8653			return self;
8654		},
8655		/**
8656		 * @summary Updates the current state to this item.
8657		 * @memberof FooGallery.Item#
8658		 * @function updateState
8659		 */
8660		updateState: function(){
8661			this.tmpl.state.update(this.tmpl.state.get(this));
8662		},
8663		/**
8664		 * @summary Converts the item to a JSON object.
8665		 * @memberof FooGallery.Item#
8666		 * @function toJSON
8667		 * @returns {object}
8668		 */
8669		toJSON: function(){
8670			return {
8671				"type": this.type,
8672				"id": this.id,
8673				"productId": this.productId,
8674				"href": this.href,
8675				"src": this.src,
8676				"srcset": this.srcset,
8677				"width": this.width,
8678				"height": this.height,
8679				"alt": this.alt,
8680				"title": this.title,
8681				"caption": this.caption,
8682				"description": this.description,
8683				"tags": this.tags.slice(),
8684				"maxCaptionLength": this.maxCaptionLength,
8685				"maxDescriptionLength": this.maxDescriptionLength,
8686				"showCaptionTitle": this.showCaptionTitle,
8687				"showCaptionDescription": this.showCaptionDescription,
8688				"noLightbox": this.noLightbox,
8689				"panelHide": this.panelHide,
8690				"attr": _obj.extend({}, this.attr)
8691			};
8692		},
8693		/**
8694		 * @summary Listens for the click event on the {@link FooGallery.Item#$anchor|$anchor} element and updates the state if enabled.
8695		 * @memberof FooGallery.Item#
8696		 * @function onAnchorClick
8697		 * @param {jQuery.Event} e - The jQuery.Event object for the click event.
8698		 * @private
8699		 */
8700		onAnchorClick: function (e) {
8701			var self = e.data.self, evt = self.tmpl.trigger("anchor-click-item", [self]);
8702			if (evt.isDefaultPrevented()) {
8703				e.preventDefault();
8704			} else {
8705				self.updateState();
8706			}
8707		},
8708		/**
8709		 * @summary Listens for the click event on the {@link FooGallery.Item#$caption|$caption}
8709 element and redirects it to the anchor if required.
8710		 * @memberof FooGallery.Item#
8711		 * @function onCaptionClick
8712		 * @param {jQuery.Event} e - The jQuery.Event object for the click event.
8713		 * @private
8714		 */
8715		onCaptionClick: function (e) {
8716			var self = e.data.self, evt = self.tmpl.trigger("caption-click-item", [self]);
8717			if (!evt.isDefaultPrevented() && self.$anchor.length > 0 && !$(e.target).is("a[href],:input")) {
8718				self.$anchor.get(0).click();
8719			}
8720		},
8721		/**
8722		 * @summary Listens for clicks on caption download buttons and triggers the client-side download helper.
8723		 * @memberof FooGallery.Item#
8724		 * @function onDownloadClick
8725		 * @param {jQuery.Event} e - The jQuery.Event object for the click event.
8726		 * @private
8727		 */
8728		onDownloadClick: function (e) {
8729			var self = e.data.self,
8730				$button = $(e.target).closest(".fg-download-button"),
8731				url = $button.attr("data-download") || $button.prop("href") || self.download;
8732
8733			if (_is.string(url) && url.length > 0 && _is.fn(_.downloadImage)) {
8734				e.preventDefault();
8735				e.stopPropagation();
8736				_.downloadImage(url).catch(function(err){
8737					console.error(err);
8738				});
8739			}
8740		}
8741	});
8742
8743	/**
8744	 * @summary A simple object containing an items' default values.
8745	 * @typedef {object} FooGallery.Item~Options
8746	 * @property {?string} [type="item"] - The `data-type` attribute for the anchor element.
8747	 * @property {?string} [id=null] - The `data-id` attribute for the outer element.
8748	 * @property {?string} [href=null] - The `href` attribute for the anchor element.
8749	 * @property {?string} [src=null] - The `src` attribute for the image element.
8750	 * @property {?string} [srcset=null] - The `srcset` attribute for the image element.
8751	 * @property {number} [width=0] - The width of the image.
8752	 * @property {number} [height=0] - The height of the image.
8753	 * @property {?string} [title=null] - The title for the image. This should be plain text.
8754	 * @property {?string} [alt=null] - The alt for the image. This should be plain text.
8755	 * @property {?string} [caption=null] - The caption for the image. This can contain HTML content.
8756	 * @property {?string} [description=null] - The description for the image. This can contain HTML content.
8757	 * @property {string[]} [tags=[]] - The `data-tags` attribute for the outer element.
8758	 * @property {number} [maxCaptionLength=0] - The max length of the title for the caption.
8759	 * @property {number} [maxDescriptionLength=0] - The max length of the description for the caption.
8760	 * @property {boolean} [showCaptionTitle=true] - Whether or not the caption title should be displayed.
8761	 * @property {boolean} [showCaptionDescription=true] - Whether or not the caption description should be displayed.
8762	 * @property {FooGallery.Item~Attributes} [attr] - Additional attributes to apply to the items' elements.
8763	 * @property {FooGallery.Item~Button[]} [buttons=[]] - An array of buttons to append to the caption.
8764	 * @property {FooGallery.Item~Ribbon} [ribbon] - The ribbon type and text to display for the item.
8765	 */
8766	_.template.configure("core", {
8767		item: {
8768			type: "item",
8769			id: "",
8770			href: "",
8771			placeholder: "",
8772			src: "",
8773			srcset: "",
8774			sources: [],
8775			width: 0,
8776			height: 0,
8777			title: "",
8778			alt: "",
8779			caption: "",
8780			description: "",
8781			tags: [],
8782			maxCaptionLength: 0,
8783			maxDescriptionLength: 0,
8784			showCaptionTitle: true,
8785			showCaptionDescription: true,
8786			noLightbox: false,
8787			panelHide: false,
8788			buttons: [],
8789			ribbon: {
8790				type: null,
8791				text: null
8792			},
8793			exif: {
8794				aperture: null,
8795				camera: null,
8796				created_timestamp: null,
8797				shutter_speed: null,
8798				focal_length: null,
8799				iso: null,
8800				orientation: null
8801			},
8802			attr: {
8803				elem: {},
8804				inner: {},
8805				anchor: {},
8806				image: {},
8807				picture: {},
8808				caption: {
8809					elem: {},
8810					inner: {},
8811					title: {},
8812					description: {}
8813				}
8814			}
8815		}
8816	}, {
8817		item: {
8818			elem: "fg-item",
8819			inner: "fg-item-inner",
8820			exif: "fg-item-exif",
8821			anchor: "fg-thumb",
8822			overlay: "fg-image-overlay",
8823			wrap: "fg-image-wrap",
8824			image: "fg-image",
8825			loader: "fg-loader",
8826			idle: "fg-idle",
8827			loading: "fg-loading",
8828			loaded: "fg-loaded",
8829			error: "fg-error",
8830			hidden: "fg-hidden",
8831			noLightbox: "fg-no-lightbox",
8832			panelHide: "fg-panel-hide",
8833			types: {
8834				item: "fg-type-unknown"
8835			},
8836			caption: {
8837				elem: "fg-caption",
8838				inner: "fg-caption-inner",
8839				title: "fg-caption-title",
8840				description: "fg-caption-desc",
8841				buttons: "fg-caption-buttons",
8842				button: "fg-caption-button"
8843			}
8844		}
8845	}, {
8846		item: {
8847			exif: {
8848				aperture: "Aperture",
8849				camera: "Camera",
8850				created_timestamp: "Date",
8851				shutter_speed: "Exposure",
8852				focal_length: "Focal Length",
8853				iso: "ISO",
8854				orientation: "Orientation"
8855			}
8856		}
8857	});
8858
8859	_.components.register("item", _.Item);
8860
8861	// ######################
8862	// ## Type Definitions ##
8863	// ######################
8864
8865	/**
8866	 * @summary An object containing properties for a button to add to the item caption.
8867	 * @typedef {object} FooGallery.Item~Button
8868	 * @property {string} url - The url the button opens.
8869	 * @property {string} text - The text displayed within the button.
8870	 * @property {string} [rel=""] - The rel attribute for the button.
8871	 * @property {string} [target="_blank"] - The target attribute for the button.
8872	 * @property {string} [classes=""] - Additional CSS class names to apply to the button.
8873	 */
8874
8875	/**
8876	 * @summary An object containing the ribbon information.
8877	 * @typedef {object} FooGallery.Item~Ribbon
8878	 * @property {string} type - The type of ribbon to display.
8879	 * @property {string} text - The text displayed within the ribbon.
8880	 */
8881
8882	/**
8883	 * @summary A simple object containing the CSS classes used by an item.
8884	 * @typedef {object} FooGallery.Item~CSSClasses
8885	 * @property {string} [elem="fg-item"] - The CSS class for the outer containing `div` element of an item.
8886	 * @property {string} [inner="fg-item-inner"] - The CSS class for the inner containing `div` element of an item.
8887	 * @property {string} [anchor="fg-thumb"] - The CSS class for the `a` element of an item.
8888	 * @property {string} [image="fg-image"] - The CSS class for the `img` element of an item.
8889	 * @property {string} [loading="fg-idle"] - The CSS class applied to an item that is waiting to be loaded.
8890	 * @property {string} [loading="fg-loading"] - The CSS class applied to an item while it is loading.
8891	 * @property {string} [loaded="fg-loaded"] - The CSS class applied to an item once it is loaded.
8892	 * @property {string} [error="fg-error"] - The CSS class applied to an item if it throws an error while loading.
8893	 * @property {object} [caption] - A simple object containing the CSS classes used by an items' caption.
8894	 * @property {string} [caption.elem="fg-caption"] - The CSS class for the outer containing `div` element of a caption.
8895	 * @property {string} [caption.inner="fg-caption-inner"] - The CSS class for the inner containing `div` element of a caption.
8896	 * @property {string} [caption.title="fg-caption-title"] - The CSS class for the title `div` element of a caption.
8897	 * @property {string} [caption.description="fg-caption-desc"] - The CSS class for the description `div` element of a caption.
8898	 */
8899	/**
8900	 * @summary A simple object used to store any additional attributes to apply to an items' elements.
8901	 * @typedef {object} FooGallery.Item~Attributes
8902	 * @property {object} [elem={}] - The attributes to apply to the items' outer `<div/>` element.
8903	 * @property {object} [inner={}] - The attributes to apply to the items' inner element.
8904	 * @property {object} [anchor={}] - The attributes to apply to the items' anchor element.
8905	 * @property {object} [image={}] - The attributes to apply to the items' image element.
8906	 * @property {object} [caption] - A simple object used to store any additional attributes to apply to an items' caption elements.
8907	 * @property {object} [caption.elem={}] - The attributes to apply to the captions' outer `<div/>` element.
8908	 * @property {object} [caption.inner={}] - The attributes to apply to the captions' inner element.
8909	 * @property {object} [caption.title={}] - The attributes to apply to the captions' title element.
8910	 * @property {object} [caption.description={}] - The attributes to apply to the captions' description element.
8911	 */
8912
8913})(
8914	FooGallery.$,
8915	FooGallery,
8916	FooGallery.utils,
8917	FooGallery.utils.is,
8918	FooGallery.utils.fn,
8919	FooGallery.utils.obj,
8920	FooGallery.utils.str
8921);
8922
8923(function($, _, _utils, _is){
8924
8925    _.Image = _.Item.extend({});
8926
8927    _.template.configure("core", null,{
8928        item: {
8929            types: {
8930                image: "fg-type-image"
8931            }
8932        }
8933    });
8934
8935    _.components.register("image", _.Image);
8936
8937})(
8938    FooGallery.$,
8939    FooGallery,
8940    FooGallery.utils,
8941    FooGallery.utils.is
8942);
8943(function(_, _is, _fn, _obj){
8944
8945	_.PagingFactory = _.Factory.extend(/** @lends FooGallery.PagingFactory */{
8946		/**
8947		 * @summary A factory for paging types allowing them to be easily registered and created.
8948		 * @memberof FooGallery
8949		 * @constructs PagingFactory
8950		 * @description The plugin makes use of an instance of this class exposed as {@link FooGallery.paging}.
8951		 * @augments FooGallery.Factory
8952		 * @borrows FooGallery.utils.Class.extend as extend
8953		 * @borrows FooGallery.utils.Class.override as override
8954		 */
8955		construct: function(){
8956			/**
8957			 * @summary An object containing all registered paging types.
8958			 * @memberof FooGallery.PagingFactory#
8959			 * @name registered
8960			 * @type {Object.<string, Object>}
8961			 * @readonly
8962			 * @example {@caption The following shows the structure of this object. The `<name>` placeholders would be the name the class was registered with.}
8963			 * {
8964			 * 	"<name>": {
8965			 * 		"name": <string>,
8966			 * 		"klass": <function>,
8967			 * 		"ctrl": <function>,
8968			 * 		"priority": <number>
8969			 * 	},
8970			 * 	"<name>": {
8971			 * 		"name": <string>,
8972			 * 		"klass": <function>,
8973			 * 		"ctrl": <function>,
8974			 * 		"priority": <number>
8975			 * 	},
8976			 * 	...
8977			 * }
8978			 */
8979			this.registered = {};
8980		},
8981		/**
8982		 * @summary Registers a paging `type` constructor with the factory using the given `name` and `test` function.
8983		 * @memberof FooGallery.PagingFactory#
8984		 * @function register
8985		 * @param {string} name - The friendly name of the class.
8986		 * @param {FooGallery.Paging} type - The paging type constructor to register.
8987		 * @param {FooGallery.PagingControl} [ctrl] - An optional control to register for the paging type.
8988		 * @param {object} [options={}] - The default options for the paging type.
8989		 * @param {object} [classes={}] - The CSS classes for the paging type.
8990		 * @param {object} [il8n={}] - The il8n strings for the paging type.
8991		 * @param {number} [priority=0] - This determines the index for the class when using either the {@link FooGallery.PagingFactory#load|load} or {@link FooGallery.PagingFactory#names|names} methods, a higher value equals a lower index.
8992		 * @returns {boolean} `true` if the `klass` was successfully registered.
8993		 */
8994		register: function(name, type, ctrl, options, classes, il8n, priority){
8995			var self = this, result = self._super(name, type, priority);
8996			if (result){
8997				var reg = self.registered;
8998				reg[name].ctrl = _is.fn(ctrl) ? ctrl : null;
8999				reg[name].opt = _is.hash(options) ? options : {};
9000				reg[name].cls = _is.hash(classes) ? classes : {};
9001				reg[name].il8n = _is.hash(il8n) ? il8n : {};
9002			}
9003			return result;
9004		},
9005		type: function(options){
9006			var self = this, opt;
9007			return _is.hash(options) && _is.hash(opt = options.paging) && _is.string(opt.type) && self.contains(opt.type) ? opt.type : null;
9008		},
9009		merge: function(options){
9010			options = _obj.extend({}, options);
9011			var self = this, type = self.type(options),
9012					reg = self.registered,
9013					def = reg["default"].opt,
9014					def_cls = reg["default"].cls,
9015					def_il8n = reg["default"].il8n,
9016					opt = _is.hash(options.paging) ? options.paging : {},
9017					cls = _is.hash(options.cls) && _is.hash(options.cls.paging) ? _obj.extend({}, options.cls.paging) : {},
9018					il8n = _is.hash(options.il8n) && _is.hash(options.il8n.paging) ? _obj.extend({}, options.il8n.paging) : {};
9019
9020			if (!_is.hash(options.cls)) options.cls = {};
9021			if (!_is.hash(options.il8n)) options.il8n = {};
9022			if (type !== "default" && self.contains(type)){
9023				options.paging = _obj.extend({}, def, reg[type].opt, opt, {type: type});
9024				options.cls = _obj.extend(options.cls, {paging: def_cls}, {paging: reg[type].cls}, {paging: cls});
9025				options.il8n = _obj.extend(options.il8n, {paging: def_il8n}, {paging: reg[type].il8n}, {paging: il8n});
9026			} else {
9027				options.paging = _obj.extend({}, def, opt, {type: type});
9028				options.cls = _obj.extend(options.cls, {paging: def_cls}, {paging: cls});
9029				options.il8n = _obj.extend(options.il8n, {paging: def_il8n}, {paging: il8n});
9030			}
9031			return options;
9032		},
9033		/**
9034		 * @summary Checks if the factory contains a control registered using the supplied `name`.
9035		 * @memberof FooGallery.PagingFactory#
9036		 * @function hasCtrl
9037		 * @param {string} name - The friendly name of the class.
9038		 * @returns {boolean}
9039		 */
9040		hasCtrl: function(name){
9041			var self = this, reg = self.registered[name];
9042			return _is.hash(reg) && _is.fn(reg.ctrl);
9043		},
9044		/**
9045		 * @summary Create a new instance of a control class registered with the supplied `name` and arguments.
9046		 * @memberof FooGallery.PagingFactory#
9047		 * @function makeCtrl
9048		 * @param {string} name - The friendly name of the class.
9049		 * @param {FooGallery.Template} template - The template creating the control.
9050		 * @param {FooGallery.Paging} parent - The parent paging class creating the control.
9051		 * @param {string} position - The position the control will be displayed at.
9052		 * @returns {?FooGallery.PagingControl}
9053		 */
9054		makeCtrl: function(name, template, parent, position){
9055			var self = this, reg = self.registered[name];
9056			if (_is.hash(reg) && _is.fn(reg.ctrl)){
9057				return new reg.ctrl(template, parent, position);
9058			}
9059			return null;
9060		}
9061	});
9062
9063	/**
9064	 * @summary The factory used to register and create the various paging types of FooGallery.
9065	 * @memberof FooGallery
9066	 * @name paging
9067	 * @type {FooGallery.PagingFactory}
9068	 */
9069	_.paging = new _.PagingFactory();
9070
9071})(
9072		FooGallery,
9073		FooGallery.utils.is,
9074		FooGallery.utils.fn,
9075		FooGallery.utils.obj
9076);
9077(function ($, _, _utils, _is) {
9078
9079	_.Paging = _.Component.extend({
9080		construct: function (template) {
9081			var self = this;
9082			/**
9083			 * @ignore
9084			 * @memberof FooGallery.Paging#
9085			 * @function _super
9086			 */
9087			self._super(template);
9088			self.opt = self.tmpl.opt.paging;
9089			self.cls = self.tmpl.cls.paging;
9090			self.il8n = self.tmpl.il8n.paging;
9091			self.sel = self.tmpl.sel.paging;
9092			self.pushOrReplace = self.opt.pushOrReplace;
9093			self.type = self.opt.type;
9094			self.theme = self.opt.theme ?? template.getCSSClass('theme');
9095			self.size = self.opt.size;
9096			self.position = self.opt.position;
9097			self.scrollToTop = self.opt.scrollToTop;
9098			self.current = 0;
9099			self.total = 0;
9100			self.ctrls = [];
9101			self._pages = [];
9102		},
9103		init: function(){},
9104		fromHash: function(hash){
9105			var parsed = parseInt(hash);
9106			return isNaN(parsed) ? null : parsed;
9107		},
9108		toHash: function(value){
9109			return _is.number(value) && value > 0 ? value.toString() : null;
9110		},
9111		getState: function(){
9112			return this.isValid(this.current) ? this.current : null;
9113		},
9114		setState: function(state){
9115			this.rebuild();
9116			var shouldScroll = false;
9117			if (!!state.item && !this.contains(state.page, state.item)){
9118				state.page = this.find(state.item);
9119				state.page = state.page !== 0 ? state.page : 1;
9120				shouldScroll = true;
9121			}
9122			this.set(state.page, shouldScroll, false, false);
9123		},
9124		destroy: function () {
9125			var self = this;
9126			self._pages.splice(0, self._pages.length);
9127			$.each(self.ctrls.splice(0, self.ctrls.length), function (i, control) {
9128				control.destroy();
9129			});
9130			self._super();
9131		},
9132		build: function () {
9133			var self = this, items = self.tmpl.items.available(),
9134				size = self.size > 0 ? self.size : items.length;
9135			// Keep an unlimited page sized to the current matches on every rebuild.
9136			self.total = self.size > 0 && items.length > 0 ? Math.ceil(items.length / self.size) : 1;
9137			for (var i = 0; i < self.total; i++) {
9138				self._pages.push(items.splice(0, size));
9139			}
9140			if (self.total > 1 && _.paging.hasCtrl(self.type)) {
9141				var pos = self.position, top, bottom;
9142				if (pos === "both" || pos === "top") {
9143					top = _.paging.makeCtrl(self.type, self.tmpl, self, "top");
9144					if (top.create()) {
9145						top.append();
9146						self.ctrls.push(top);
9147					}
9148				}
9149				if (pos === "both" || pos === "bottom") {
9150					bottom = _.paging.makeCtrl(self.type, self.tmpl, self, "bottom");
9151					if (bottom.create()) {
9152						bottom.append();
9153						self.ctrls.push(bottom);
9154					}
9155				}
9156			}
9157		},
9158		rebuild: function () {
9159			var self = this;
9160			self.current = 0;
9161			self.total = 0;
9162			self._pages.splice(0, self._pages.length);
9163			$.each(self.ctrls.splice(0, self.ctrls.length), function (i, control) {
9164				control.destroy();
9165			});
9166			self.build();
9167		},
9168		all: function () {
9169			return this._pages.slice();
9170		},
9171		available: function () {
9172			return this.get(this.current);
9173		},
9174		controls: function (pageNumber) {
9175			var self = this;
9176			if (self.isValid(pageNumber)) {
9177				$.each(self.ctrls, function (i, control) {
9178					control.update(pageNumber);
9179				});
9180			}
9181		},
9182		isValid: function (pageNumber) {
9183			return _is.number(pageNumber) && pageNumber > 0 && pageNumber <= this.total;
9184		},
9185		number: function (value) {
9186			return this.isValid(value) ? value : (this.current === 0 ? 1 : this.current);
9187		},
9188		create: function (pageNumber, isFilter) {
9189			var self = this;
9190			pageNumber = self.number(pageNumber);
9191
9192			var pageIndex = pageNumber - 1, pageItems = self._pages[pageIndex], detach;
9193			if (isFilter){
9194				detach = self.tmpl.items.all();
9195			} else {
9196				detach = self._pages.reduce(function(detach, page, index){
9197					return index === pageIndex ? detach : detach.concat(page);
9198				}, self.tmpl.items.unavailable());
9199			}
9200
9201			self.current = pageNumber;
9202			self.tmpl.items.detach(detach);
9203			self.tmpl.items.create(pageItems, true);
9204		},
9205		get: function (pageNumber) {
9206			var self = this;
9207			if (self.isValid(pageNumber)) {
9208				pageNumber = self.number(pageNumber);
9209				return self._pages[pageNumber - 1];
9210			}
9211			return [];
9212		},
9213		set: function (pageNumber, scroll, updateState, isFilter) {
9214			var self = this;
9215			if (self.isValid(pageNumber)) {
9216				self.controls(pageNumber);
9217				var num = self.number(pageNumber), state;
9218				if (num !== self.current) {
9219					var prev = self.current, setPage = function () {
9220						updateState = _is.boolean(updateState) ? updateState : true;
9221						isFilter = _is.boolean(isFilter) ? isFilter : false;
9222						if (updateState && self.current === 1 && !self.tmpl.state.exists()) {
9223							state = self.tmpl.state.get();
9224							self.tmpl.state.update(state, self.pushOrReplace);
9225						}
9226						self.create(num, isFilter);
9227						if (updateState) {
9228							state = self.tmpl.state.get();
9229							self.tmpl.state.update(state, self.pushOrReplace);
9230						}
9231						self.tmpl.trigger("page-change", [self.current, prev, isFilter]);
9232						if (self.scrollToTop && _is.boolean(scroll) ? scroll : false) {
9233							var page = self.get(self.current);
9234							if (page.length > 0) {
9235								page[0].scrollTo("top");
9236							}
9237						}
9238						self.tmpl.trigger("after-page-change", [self.current, prev, isFilter]);
9239					};
9240					var e = self.tmpl.trigger("before-page-change", [self.current, num, setPage, isFilter]);
9241					if (e.isDefaultPrevented()) return false;
9242					setPage();
9243					return true;
9244				}
9245			}
9246			return false;
9247		},
9248		find: function (item) {
9249			var self = this;
9250			for (var i = 0, l = self._pages.length; i < l; i++) {
9251				if (_utils.inArray(item, self._pages[i]) !== -1) {
9252					return i + 1;
9253				}
9254			}
9255			return 0;
9256		},
9257		contains: function (pageNumber, item) {
9258			var items = this.get(pageNumber);
9259			return _utils.inArray(item, items) !== -1;
9260		},
9261		first: function () {
9262			this.goto(1);
9263		},
9264		last: function () {
9265			this.goto(this._pages.length);
9266		},
9267		prev: function () {
9268			this.goto(this.current - 1);
9269		},
9270		next: function () {
9271			this.goto(this.current + 1);
9272		},
9273		goto: function (pageNumber) {
9274			this.set(pageNumber, true);
9275		}
9276	});
9277
9278	_.PagingControl = _.Component.extend({
9279		construct: function (template, parent, position) {
9280			var self = this;
9281			self._super(template);
9282			self.pages = parent;
9283			self.position = position;
9284			self.$container = null;
9285			self._containerExisted = false;
9286			self._placeholderClasses = [];
9287		},
9288		create: function () {
9289			var self = this;
9290			self.$container = $("#" + self.tmpl.id + "_paging-" + self.position);
9291			if (self.$container.length > 0){
9292				self._containerExisted = true;
9293				self.$container.removeClass(function(i, classNames){
9294					self._placeholderClasses = classNames.match(/(^|\s)fg-ph-\S+/g) || [];
9295					return self._placeholderClasses.join(' ');
9296				}).addClass([self.pages.cls.container, self.pages.theme].join(' '));
9297			} else {
9298				self.$container = $("<nav/>", {"class": [self.pages.cls.container, self.pages.theme].join(' ')});
9299			}
9300			return true;
9301		},
9302		destroy: function () {
9303			var self = this;
9304			if (self._containerExisted){
9305				self.$container.empty()
9306					.removeClass()
9307					.addClass(self._placeholderClasses.join(' '));
9308			} else {
9309				self.$container.remove();
9310			}
9311			self.$container = null;
9312		},
9313		append: function () {
9314			var self = this;
9315			if (self._containerExisted) return;
9316			if (self.position === "top") {
9317				self.$container.insertBefore(self.tmpl.$el);
9318			} else {
9319				self.$container.insertAfter(self.tmpl.$el);
9320			}
9321		},
9322		update: function (pageNumber) {
9323		}
9324	});
9325
9326	_.paging.register("default", _.Paging, null, {
9327		type: "none",
9328		theme: null,
9329		size: 30,
9330		pushOrReplace: "push",
9331		position: "none",
9332		scrollToTop: true
9333	}, {
9334		container: "fg-paging-container"
9335	}, null, -100);
9336
9337})(
9338		FooGallery.$,
9339		FooGallery,
9340		FooGallery.utils,
9341		FooGallery.utils.is
9342);
9343
9344(function($, _, _utils, _is){
9345
9346	_.Dots = _.Paging.extend({});
9347
9348	_.DotsControl = _.PagingControl.extend({
9349		construct: function(template, parent, position){
9350			this._super(template, parent, position);
9351			this.$list = null;
9352			this.$items = null;
9353		},
9354		create: function(){
9355			var self = this;
9356			if (self._super()){
9357				var cls = self.pages.cls, il8n = self.pages.il8n,
9358					items = [], $list = $("<ul/>", {"class": cls.list});
9359
9360				for (var i = 0, l = self.pages.total, $item; i < l; i++){
9361					items.push($item = self.createItem(i + 1, il8n.page));
9362					$list.append($item);
9363				}
9364				self.$list = $list;
9365				self.$items = $($.map(items, function($item){ return $item.get(); }));
9366				self.$container.append($list);
9367				return true;
9368			}
9369			return false;
9370		},
9371		destroy: function(){
9372			var self = this, sel = self.pages.sel;
9373			self.$list.find(sel.link).off("click.foogallery", self.onLinkClick);
9374			self.$list = $();
9375			self.$items = $();
9376			self._super();
9377		},
9378		update: function(pageNumber){
9379			this.setSelected(pageNumber - 1);
9380		},
9381		setSelected: function(index){
9382			var self = this, cls = self.pages.cls, il8n = self.pages.il8n, sel = self.pages.sel;
9383			// first find any previous selected items and deselect them
9384			self.$items.filter(sel.selected).removeClass(cls.selected).each(function (i, el) {
9385				// we need to revert the original items screen-reader text if it existed as being selected sets it to the value of the labels.current option
9386				var $item = $(el), label = $item.data("label"), $sr = $item.find(sel.reader);
9387				// if we have an original value and a screen-reader element then update it
9388				if (_is.string(label) && $sr.length !== 0) {
9389					$sr.html(label);
9390				}
9391			});
9392			// next find the newly selected item and set it as selected
9393			self.$items.eq(index).addClass(cls.selected).each(function (i, el) {
9394				// we need to update the items screen-reader text to appropriately show it as selected using the value of the labels.current option
9395				var $item = $(el), $sr = $item.find(sel.reader), label = $sr.html();
9396				// if we have a current label to backup and a screen-reader element then update it
9397				if (_is.string(label) && $sr.length !== 0) {
9398					// store the original screen-reader text so we can revert it later
9399					$item.data("label", label);
9400					$sr.html(il8n.current);
9401				}
9402			});
9403		},
9404		/**
9405		 * @summary Create and return a jQuery object containing a single `li` and its' link.
9406		 * @memberof FooGallery.DotsControl#
9407		 * @function createItem
9408		 * @param {(number|string)} pageNumber - The page number for the item.
9409		 * @param {string} [label=""] - The label that is displayed when hovering over an item.
9410		 * @param {string} [text=""] - The text to display for the item, if not supplied this defaults to the `pageNumber` value.
9411		 * @param {string} [classNames=""] - A space separated list of CSS class names to apply to the item.
9412		 * @param {string} [sr=""] - The text to use for screen readers, if not supplied this defaults to the `label` value.
9413		 * @returns {jQuery}
9414		 */
9415		createItem: function(pageNumber, label, text, classNames, sr){
9416			text = _is.string(text) ? text : pageNumber;
9417			label = _is.string(label) ? label : "";
9418			var self = this, opt = self.pages.opt, cls = self.pages.cls;
9419			var $link = $("<a/>", {"class": cls.link, "href": "#page-" + pageNumber}).html(text).on("click.foogallery", {self: self, page: pageNumber}, self.onLinkClick);
9420			if (!_is.empty(label)){
9421				$link.attr("title", label.replace(/\{PAGE}/g, pageNumber).replace(/\{LIMIT}/g, opt.limit + ""));
9422			}
9423			sr = _is.string(sr) ? sr : label;
9424			if (!_is.empty(sr)){
9425				$link.prepend($("<span/>", {"class":cls.reader, text: sr.replace(/\{PAGE}/g, "").replace(/\{LIMIT}/g, opt.limit + "")}));
9426			}
9427			var $item = $("<li/>", {"class": cls.item}).append($link);
9428			classNames = _is.string(classNames) ? classNames : "";
9429			if (!_is.empty(classNames)){
9430				$item.addClass(classNames);
9431			}
9432			return $item;
9433		},
9434		/**
9435		 * @summary Handles the click event of the dots links.
9436		 * @memberof FooGallery.DotsControl#
9437		 * @function onLinkClick
9438		 * @param {jQuery.Event} e - The jQuery.Event object for the click event.
9439		 * @private
9440		 */
9441		onLinkClick: function(e){
9442			e.preventDefault();
9443			var self = e.data.self, page = e.data.page, sel = self.pages.sel;
9444			// this check should not be required as we use the CSS pointer-events: none; property on disabled links but just in case test for the class here
9445			if (!$(this).closest(sel.item).is(sel.disabled)){
9446				self.pages.set(page, true);
9447			}
9448		}
9449	});
9450
9451	_.paging.register("dots", _.Dots, _.DotsControl, {
9452		type: "dots",
9453		position: "both",
9454		pushOrReplace: "push"
9455	}, {
9456		list: "fg-dots",
9457		item: "fg-dot-item",
9458		link: "fg-dot-link",
9459		disabled: "fg-disabled",
9460		selected: "fg-selected",
9461		visible: "fg-visible",
9462		reader: "fg-sr-only"
9463	}, {
9464		current: "Current page",
9465		page: "Page {PAGE}"
9466	});
9467
9468})(
9469	FooGallery.$,
9470	FooGallery,
9471	FooGallery.utils,
9472	FooGallery.utils.is
9473);
9474(function($, _, _utils, _is, _obj, _fn, _t){
9475
9476    var instance_id = 0;
9477
9478    /**
9479     * @memberof FooGallery.
9480     * @class Panel
9481     * @param template
9482     * @param options
9483     * @param classes
9484     * @param il8n
9485     * @augments FooGallery.Component
9486     */
9487    _.Panel = _.Component.extend(/** @lends FooGallery.Panel */{
9488        /**
9489         * @constructs
9490         * @param template
9491         * @param options
9492         * @param classes
9493         * @param il8n
9494         */
9495        construct: function(template, options, classes, il8n){
9496            var self = this;
9497            self.instanceId = ++instance_id;
9498            self._super(template);
9499
9500            self.opt = _obj.extend({}, self.tmpl.opt.panel, options);
9501
9502            self.cls = _obj.extend({}, self.tmpl.cls.panel, classes);
9503
9504            self.il8n = _obj.extend({}, self.tmpl.il8n.panel, il8n);
9505
9506            var states = self.cls.states;
9507            self.cls.states.all = Object.keys(states).map(function (key) {
9508                return states[key];
9509            }).join(" ");
9510            self.cls.states.allLoading = [states.idle, states.loading, states.loaded, states.error].join(" ");
9511            self.cls.states.allProgress = [states.idle, states.started, states.stopped, states.paused].join(" ");
9512
9513            self.sel = _utils.selectify(self.cls);
9514
9515            self.videoSources = !_is.undef(_.Panel.Video) ? _.Panel.Video.sources.load(self) : [];
9516
9517            self.buttons = new _.Panel.Buttons(self);
9518
9519            self.content = new _.Panel.Content(self);
9520            self.info = new _.Panel.Info(self);
9521            self.thumbs = new _.Panel.Thumbs(self);
9522
9523            self.areas = [self.content, self.info, self.thumbs];
9524
9525            if ( _.Panel.Cart ){
9526                self.cart = new _.Panel.Cart(self);
9527                self.areas.push( self.cart );
9528            }
9529
9530            if ( _.Panel.Comments ){
9531                self.comments = new _.Panel.Comments(self);
9532                self.areas.push( self.comments );
9533            }
9534
9535            // Make sure only one side area can occupy the same position, overrides supplied options
9536            // There can be only one...
9537            const highlanders = self.areas.filter( a => a instanceof _.Panel.SideArea && a.isVisible && a.opt.group === 'overlay' );
9538            while ( highlanders.length > 0 ) {
9539                const theOne = highlanders.shift();
9540                highlanders.forEach( challenger => {
9541                    if ( challenger.isTargetingSamePosition( theOne ) ) {
9542                        challenger.isVisible = false;
9543                        challenger.button.isPressed = false;
9544                    }
9545                } );
9546            }
9547
9548            self.$el = null;
9549
9550            self.el = null;
9551
9552            self.isCreated = false;
9553
9554            self.isDestroyed = false;
9555
9556            self.isDestroying = false;
9557
9558            self.isAttached = false;
9559
9560            self.isLoading = false;
9561
9562            self.isLoaded = false;
9563
9564            self.isError = false;
9565
9566            self.isInline = false;
9567
9568            self.isMaximized = false;
9569
9570            self.isFullscreen = false;
9571
9572            self.hasTransition = !_is.empty(self.cls.transition[self.opt.transition]);
9573
9574            self.currentItem = null;
9575
9576            self.prevItem = null;
9577
9578            self.nextItem = null;
9579
9580            self.lastBreakpoint = null;
9581
9582            self.isSmallScreen = false;
9583            self.isMediumScreen = false;
9584            self.isLargeScreen = false;
9585            self.isMobileLayout = false;
9586
9587            self.breakpointClassNames = self.opt.breakpoints.map(function(bp){
9588                return "fg-" + bp.name + " fg-" + bp.name + "-width" + " fg-" + bp.name + "-height";
9589            }).concat(["fg-landscape","fg-portrait"]).join(" ");
9590
9591            self.robserver = new ResizeObserver(_fn.throttle(function (entries) {
9592                if (!self.destroying && !self.destroyed){
9593                    entries.forEach(function (entry) {
9594                        if (entry.target === self.el){
9595                            var size = _utils.getResizeObserverSize(entry);
9596                            self.onResize(size.width, size.height);
9597                        }
9598                    });
9599                }
9600            }, 50));
9601
9602            self.__media = {};
9603
9604            self.__loading = null;
9605
9606            if (!(self.tmpl.destroying || self.tmpl.destroyed)){
9607                self.tmpl.on({
9608                    "after-filter-change": self.onItemsChanged
9609                }, self);
9610            }
9611        },
9612        isVisible: function(item){
9613            return item instanceof _.Item && !item.noLightbox && !item.panelHide;
9614        },
9615        onItemsChanged: function(e){
9616            var self = this;
9617            if (self.thumbs.isCreated && self.tmpl.initialized){
9618                self.thumbs.doCreateThumbs(self.tmpl.items.available(self.isVisible));
9619                if (self.isAttached) self.load(self.tmpl.items.first(self.isVisible));
9620            }
9621        },
9622        create: function(){
9623            var self = this;
9624            if (!self.isCreated) {
9625                var e = self.trigger("create");
9626                if (!e.isDefaultPrevented()) {
9627                    self.isCreated = self.doCreate();
9628                }
9629                if (self.isCreated) {
9630                    self.trigger("created");
9631                }
9632            }
9633            return self.isCreated;
9634        },
9635        doCreate: function(){
9636            var self = this;
9637            self.$el = self.createElem();
9638            self.el = self.$el.get(0);
9639            if (self.tmpl.opt.protected){
9640                self.el.oncontextmenu = function(e){
9641                    e.preventDefault();
9642                    return false;
9643                };
9644            }
9645            if (self.opt.keyboard){
9646                self.$el.attr("tabindex", -1).on("keydown.foogallery", {self: self}, self.onKeyDown);
9647            }
9648            self.buttons.appendTo( self.$el );
9649            self.areas.forEach(function(area){
9650                area.appendTo( self.$el );
9651            });
9652            return true;
9653        },
9654        createElem: function(){
9655            var self = this, transition = self.cls.transition[self.opt.transition] || "";
9656            self.hasTransition = !_is.empty(transition);
9657            var classes = [
9658                self.cls.elem,
9659                transition,
9660                self.cls.buttons.portrait[self.opt.buttonsPortrait] || "",
9661                self.cls.buttons.landscape[self.opt.buttonsLandscape] || "",
9662                _is.string(self.opt.theme) ? self.opt.theme : self.tmpl.getCSSClass("theme", "fg-dark"),
9663                _is.string(self.opt.loadingIcon) ? self.opt.loadingIcon : self.tmpl.getCSSClass("loadingIcon"),
9664                _is.string(self.opt.hoverIcon) ? self.opt.hoverIcon : self.tmpl.getCSSClass("hoverIcon"),
9665                _is.string(self.opt.videoIcon) ? self.opt.videoIcon : self.tmpl.getCSSClass("videoIcon"),
9666                _is.boolean(self.opt.stickyVideoIcon) && self.opt.stickyVideoIcon ? self.cls.stickyVideoIcon : self.tmpl.getCSSClass("stickyVideoIcon"),
9667                _is.string(self.opt.insetShadow) ? self.opt.insetShadow : self.tmpl.getCSSClass("insetShadow"),
9668                _is.string(self.opt.filter) ? self.opt.filter : self.tmpl.getCSSClass("filter"),
9669                _is.string(self.opt.hoverColor) ? self.opt.hoverColor : self.tmpl.getCSSClass("hoverColor"),
9670                _is.boolean(self.opt.hoverScale) && self.opt.hoverScale ? self.cls.hoverScale : self.tmpl.getCSSClass("hoverScale"),
9671                _is.string(self.opt.button) ? self.opt.button : "",
9672                _is.string(self.opt.highlight) ? self.opt.highlight : "",
9673                self.opt.adjustBackgroundColor ? self.cls.dominantColor : "",
9674                self.opt.stackSideAreas ? self.cls.stackSideAreas : "",
9675                self.opt.preserveButtonSpace ? self.cls.preserveButtonSpace : "",
9676                self.opt.fitMedia ? self.cls.fitMedia : "",
9677                self.opt.noMobile ? self.cls.noMobile : "",
9678                self.opt.hoverButtons ? self.cls.hoverButtons : "",
9679                self.opt.classNames
9680            ];
9681            return $('<div/>
9681').addClass(classes.join(" "));
9682        },
9683        destroy: function () {
9684            var self = this, _super = self._super.bind(self);
9685            if (self.isDestroyed) return _fn.resolved;
9686            self.isDestroying = true;
9687            return $.Deferred(function (def) {
9688                if (self.isLoading && _is.promise(self.__loading)) {
9689                    self.__loading.always(function () {
9690                        var e = self.trigger("destroy");
9691                        self.isDestroying = false;
9692                        if (!e.isDefaultPrevented()) {
9693                            self.isDestroyed = self.doDestroy();
9694                        }
9695                        if (self.isDestroyed) {
9696                            self.trigger("destroyed");
9697                        }
9698                        def.resolve();
9699                    });
9700                } else {
9701                    var e = self.trigger("destroy");
9702                    self.isDestroying = false;
9703                    if (!e.isDefaultPrevented()) {
9704                        self.isDestroyed = self.doDestroy();
9705                    }
9706                    if (self.isDestroyed) {
9707                        self.trigger("destroyed");
9708                    }
9709                    def.resolve();
9710                }
9711            }).then(function(){
9712                _super();
9713            }).promise();
9714        },
9715        doDestroy: function(){
9716            var self = this;
9717            self.buttons.destroy();
9718            self.areas.reverse();
9719            self.areas.forEach(function (area) {
9720                area.destroy();
9721            });
9722            self.detach();
9723            if (self.isCreated){
9724                self.$el.remove();
9725            }
9726            return true;
9727        },
9728        appendTo: function( parent ){
9729            var self = this;
9730            if ((self.isCreated || self.create()) && !self.isAttached){
9731                var e = self.trigger("append", [parent]);
9732                if (!e.isDefaultPrevented()) {
9733                    self.isAttached = self.doAppendTo( parent );
9734                }
9735                if (self.isAttached) {
9736                    self.trigger("appended", [parent]);
9737                }
9738            }
9739            return self.isAttached;
9740        },
9741        doAppendTo: function( parent ){
9742            var self = this, $parent = $( parent ), maximize = self.buttons.get("maximize");
9743            self.isInline = !$parent.is("body");
9744            self.$el.appendTo( $parent );
9745
9746            maximize.set(!self.isInline, self.isInline && maximize.isEnabled());
9747
9748            self.robserver.observe(self.el);
9749
9750            self.areas.forEach(function (area) {
9751                area.listen();
9752            });
9753            return self.el.parentNode !== null;
9754        },
9755        detach: function(){
9756            var self = this;
9757            if (self.isCreated && self.isAttached) {
9758                var e = self.trigger("detach");
9759                if (!e.isDefaultPrevented()) {
9760                    self.isAttached = !self.doDetach();
9761                }
9762                if (!self.isAttached) {
9763                    self.trigger("detached");
9764                }
9765            }
9766            return !self.isAttached;
9767        },
9768        doDetach: function(){
9769            var self = this;
9770            self.robserver.unobserve(self.el);
9771            self.areas.forEach(function (area) {
9772                area.stopListening();
9773            });
9774            self.$el.detach();
9775            return true;
9776        },
9777        resize: function(){
9778            var self = this;
9779            self.$el.removeClass(self.breakpointClassNames).addClass(self.lastBreakpoint);
9780            self.isMediumScreen = self.$el.hasClass("fg-medium");
9781            self.isLargeScreen = self.$el.hasClass("fg-large");
9782            self.isXLargeScreen = self.$el.hasClass("fg-x-large");
9783            self.isSmallScreen = !self.isMediumScreen && !self.isLargeScreen && !self.isXLargeScreen;
9784            self.isMobileLayout = self.isSmallScreen && !self.opt.noMobile;
9785            self.areas.forEach(function (area) {
9786                area.resize();
9787            });
9788            self.buttons.resize();
9789        },
9790        onResize: function(width, height){
9791            var self = this, bp = self.getBreakpoint(width, height);
9792            if (self.lastBreakpoint !== bp){
9793                self.lastBreakpoint = bp;
9794                self.resize();
9795            }
9796        },
9797        getBreakpoint: function(width, height){
9798            var self = this,
9799                result = [];
9800
9801            self.opt.breakpoints.forEach(function(bp){
9802                var w = bp.width <= width, h = bp.height <= height;
9803                if (w && h) result.push("fg-" + bp.name);
9804                if (w) result.push("fg-" + bp.name + "-width");
9805                if (h) result.push("fg-" + bp.name + "-height");
9806            });
9807
9808            result.push(width > height ? "fg-landscape" : "fg-portrait");
9809
9810            return result.length > 0 ? result.join(" ") : null;
9811        },
9812        getMedia: function(item){
9813            if (!(item instanceof _.Item)) return null;
9814            if (this.__media.hasOwnProperty(item.id)) return this.__media[item.id];
9815            return this.__media[item.id] = _.Panel.media.make(item.type, this, item);
9816        },
9817        getItem: function(item){
9818            var self = this, result = item;
9819            if (!(result instanceof _.Item)) result = self.currentItem;
9820            if (!(result instanceof _.Item)) result = self.tmpl.items.first(self.isVisible);
9821            if (item instanceof _.Item && !self.isVisible(item)){
9822                result = self.tmpl.items.next(item, self.isVisible, self.opt.loop);
9823                if (!(result instanceof _.Item)){
9824                    result = self.tmpl.items.prev(item, self.isVisible, self.opt.loop);
9825                }
9826            }
9827            return result;
9828        },
9829        load: function( item ){
9830            var self = this;
9831
9832            item = self.getItem(item);
9833
9834            if (!(item instanceof _.Item)) return _fn.reject("no item to load");
9835            if (item === self.currentItem) return _fn.reject("item is currently loaded");
9836
9837            self.isLoading = true;
9838            self.isLoaded = false;
9839            self.isError = false;
9840
9841            return self.__loading = $.Deferred(function(def){
9842                if (!self.isCreated || !self.isAttached){
9843                    def.rejectWith("not created or attached");
9844                    return;
9845                }
9846                var media = self.getMedia(item);
9847                if (!(media instanceof _.Panel.Media)){
9848                    def.rejectWith("no media to load");
9849                    return;
9850                }
9851                var e = self.trigger("load", [media, item]);
9852                if (e.isDefaultPrevented()){
9853                    def.rejectWith("default prevented");
9854                    return;
9855                }
9856                self.currentItem = item;
9857                self.prevItem = self.tmpl.items.prev(item, self.isVisible, self.opt.loop);
9858                self.nextItem = self.tmpl.items.next(item, self.isVisible, self.opt.loop);
9859                self.updateBackgroundColor(item);
9860                self.doLoad(media).then(def.resolve).fail(def.reject);
9861            }).always(function(){
9862                self.isLoading = false;
9863            }).then(function(){
9864                self.isLoaded = true;
9865                self.trigger("loaded", [item]);
9866                item.updateState();
9867            }).fail(function(){
9868                self.isError = true;
9869                self.trigger("error", [item]);
9870            }).promise();
9871        },
9872        updateBackgroundColor: function( item ){
9873            var self = this, color = "";
9874            if (!self.opt.adjustBackgroundColor || !self.el) return;
9875            if (item instanceof _.Item && _is.element(item.el)){
9876                color = item.el.style.getPropertyValue("--fg-lightbox-dominant-color");
9877                if (_is.empty(color) && window.getComputedStyle){
9878                    color = window.getComputedStyle(item.el).getPropertyValue("--fg-lightbox-dominant-color");
9879                }
9880            }
9881            color = _is.string(color) ? color.trim() : "";
9882            if (_is.empty(color)){
9883                self.el.style.removeProperty("--fg-dominant-color");
9884            } else {
9885                self.el.style.setProperty("--fg-dominant-color", color);
9886            }
9887        },
9888        doLoad: function( media ){
9889            var self = this, wait = [];
9890            self.buttons.beforeLoad(media);
9891            self.areas.forEach(function (area) {
9892                wait.push(area.load(media));
9893            });
9894            return $.when.apply($, wait).then(function(){
9895                self.buttons.afterLoad(media);
9896            }).promise();
9897        },
9898        open: function( item, parent ){
9899            var self = this;
9900            item = self.getItem(item);
9901            var e = self.trigger("open", [item, parent]);
9902            if (e.isDefaultPrevented()) return _fn.reject("default prevented");
9903            return self.doOpen(item, parent).then(function(){
9904                self.trigger("opened", [item, parent]);
9905            });
9906        },
9907        doOpen: function( item, parent ){
9908            var self = this;
9909            return $.Deferred(function(def){
9910                if (!(item instanceof _.Item)){
9911                    def.rejectWith("item not instanceof FooGallery.Item");
9912                    return;
9913                }
9914                parent = !_is.empty(parent) ? parent : "body";
9915                if (!self.isAttached){
9916                    self.appendTo( parent );
9917                }
9918                if (self.isAttached){
9919                    self.load( item ).then(def.resolve).fail(def.reject);
9920                } else {
9921                    def.rejectWith("not attached");
9922                }
9923            }).promise();
9924        },
9925        next: function(){
9926            var self = this, current = self.currentItem, next = self.nextItem;
9927            if (!(next instanceof _.Item)) return _fn.reject("no next item");
9928            var e = self.trigger("next", [current, next]);
9929            if (e.isDefaultPrevented()) return _fn.reject("default prevented");
9930            return self.doNext(next).then(function(){
9931                self.trigger("after-next", [current, next]);
9932            });
9933        },
9934        doNext: function(item){
9935            return this.load( item );
9936        },
9937        prev: function(){
9938            var self = this, current = self.currentItem, prev = self.prevItem;
9939            if (!(prev instanceof _.Item)) return _fn.reject("no prev item");
9940            var e = self.trigger("prev", [current, prev]);
9941            if (e.isDefaultPrevented()) return _fn.reject("default prevented");
9942            return self.doPrev(prev).then(function(){
9943                self.trigger("after-prev", [current, prev]);
9944            });
9945        },
9946        doPrev: function(item){
9947            return this.load( item );
9948        },
9949        close: function(immediate){
9950            var self = this;
9951            if ( self.isClosing ) return $.Deferred().reject();
9952            var e = self.trigger("close", [self.currentItem]);
9953            if (e.isDefaultPrevented()) return _fn.reject("default prevented");
9954            return self.doClose(immediate).then(function(){
9955                self.trigger("closed");
9956            });
9957        },
9958        doClose: function(immediate, detach){
9959            detach = _is.boolean(detach) ? detach : true;
9960            var self = this;
9961            self.isClosing = true;
9962            return $.Deferred(function(def){
9963                var closeArea = function(area){
9964                    try {
9965                        return area.close(immediate);
9966                    } catch (err){
9967                        return _fn.reject(err);
9968                    }
9969                };
9970                $.when(closeArea(self.content)).always(function(){
9971                    var wait = [];
9972                    self.areas.forEach(function(area){
9973                        if (area !== self.content){
9974                            wait.push(closeArea(area));
9975                        }
9976                    });
9977                    _fn.allSettled(wait).then(function(){
9978                        def.resolve();
9979                    });
9980                });
9981            }).always(function(){
9982                self.isClosing = false;
9983                self.currentItem = null;
9984                if (self.opt.adjustBackgroundColor && self.el){
9985                    self.el.style.removeProperty("--fg-dominant-color");
9986                }
9987                self.buttons.close();
9988                if (detach) self.detach();
9989                self.tmpl.state.clear();
9990            }).promise();
9991        },
9992        trapFocus: function(){
9993            if (!this.isCreated) return;
9994            this.$el.on('keydown', {self: this}, this.onTrapFocusKeydown);
9995        },
9996        releaseFocus: function(){
9997            if (!this.isCreated) return;
9998            this.$el.off('keydown', this.onTrapFocusKeydown);
9999        },
10000        onTrapFocusKeydown: function(e){
10001            // If TAB key pressed
10002            if (e.keyCode === 9) {
10003                var self = e.data.self,
10004                    $target = $(e.target),
10005                    targetIsDialog = $target.is('[role=dialog]'),
10006                    $dialog = targetIsDialog ? $target : $target.parents('[role=dialog]');
10007
10008                // If inside a Modal dialog (determined by attribute role="dialog")
10009                if ($dialog.length) {
10010                    // Find first or last input element in the dialog parent (depending on whether Shift was pressed).
10011                    var $focusable = $dialog.find(self.opt.focusable.include).not(self.opt.focusable.exclude),
10012                        $first = $focusable.first(), $last = $focusable.last(),
10013                        $boundary = e.shiftKey ? $first : $last,
10014                        $new = e.shiftKey ? $last : $first;
10015
10016                    if ($boundary.length && ( targetIsDialog || $target.is($boundary) )) {
10017                        e.preventDefault();
10018                        $new.trigger('focus');
10019                    }
10020                }
10021            }
10022        },
10023        onKeyDown: function(e){
10024            var self = e.data.self;
10025            switch (e.which){
10026                case 39: case 40: self.next(); break;
10027                case 37: case 38: self.prev(); break;
10028                case 27:
10029                    var button;
10030                    if (self.isFullscreen){
10031                        button = self.buttons.get("fullscreen");
10032                        button.exit();
10033                    } else if (self.isMaximized && self.isInline){
10034                        button = self.buttons.get("maximize");
10035                        button.exit();
10036                    } else if (self.opt.buttons.close) {
10037                        self.close();
10038                    }
10039                    break;
10040            }
10041        }
10042    });
10043
10044
10045    _.template.configure("core", {
10046        panel: {
10047            classNames: "",
10048            noMobile: false,
10049            hoverButtons: false,
10050            icons: "default",
10051            transition: "none", // none | fade | horizontal | vertical
10052
10053            // the below are CSS class names to use, if not supplied the lightbox will inherit the value from the gallery
10054            theme: null,
10055            button: null,
10056            highlight: null,
10057            loadingIcon: null,
10058            hoverIcon: null,
10059            videoIcon: null,
10060            stickyVideoIcon: null,
10061            hoverColor: null,
10062            hoverScale: null,
10063            insetShadow: null,
10064            filter: null,
10065
10066            loop: true,
10067            autoProgress: 0,
10068            autoProgressStart: true,
10069            autoProgressVisible: true,
10070            fitMedia: false,
10071            adjustBackgroundColor: false,
10072            keyboard: true,
10073            noScrollbars: true,
10074            swipe: true,
10075            stackSideAreas: true,
10076            preserveButtonSpace: true,
10077            buttonsPortrait: "top", // top | bottom
10078            buttonsLandscape: "right", // left | right
10079            admin: false,
10080
10081            info: "bottom", // none | top | bottom | left | right
10082            infoVisible: false,
10083            infoOverlay: true,
10084            infoAutoHide: true,
10085            infoAlign: "default", // default | left | center | right | justified
10086            exif: "none", // none | full | partial | minimal
10087
10088            cart: "none", // none | top | bottom | left | right
10089            cartAutoHide: true,
10090            cartVisible: false,
10091            cartOriginal: false,
10092            cartOverlay: true,
10093            cartAjax: null,
10094            cartNonce: null,
10095            cartTimeout: null,
10096
10097            thumbs: "none", // none | top | bottom | left | right
10098            thumbsVisible: true,
10099            thumbsCaptions: true,
10100            thumbsCaptionsAlign: "default", // default | left | center | right | justified
10101            thumbsSmall: false,
10102            thumbsBestFit: true,
10103
10104            focusable: {
10105                include: 'a[href], area[href], input, select, textarea, button, iframe, object, embed, [tabindex], [contenteditable]',
10106                exclude: '[tabindex=-1], [disabled], :hidden'
10107            },
10108
10109            buttons: {
10110                prev: true,
10111                next: true,
10112                close: true,
10113                maximize: true,
10114                fullscreen: true,
10115                autoProgress: true,
10116                info: true,
10117                thumbs: false,
10118                cart: true,
10119                download: false
10120            },
10121            breakpoints: [{
10122                name: "medium",
10123                width: 800,
10124                height: 800
10125            },{
10126                name: "large",
10127                width: 1024,
10128                height: 1024
10129            },{
10130                name: "x-large",
10131                width: 1280,
10132                height: 1280
10133            }]
10134        }
10135    },{
10136        panel: {
10137            elem: "fg-panel",
10138            maximized: "fg-panel-maximized",
10139            fullscreen: "fg-panel-fullscreen",
10140
10141            fitMedia: "fg-panel-fit-media",
10142            noScrollbars: "fg-panel-no-scroll",
10143            stackSideAreas: "fg-panel-area-stack",
10144            preserveButtonSpace: "fg-panel-preserve-button-space",
10145            hoverButtons: "fg-panel-hover-buttons",
10146            stickyVideoIcon: "fg-video-sticky",
10147            hoverScale: "fg-hover-scale",
10148            noMobile: "fg-panel-no-mobile",
10149            dominantColor: "fg-dominant-color",
10150
10151            loader: "fg-loader",
10152
10153            states: {
10154                idle: "fg-idle",
10155                loading: "fg-loading",
10156                loaded: "fg-loaded",
10157                error: "fg-error",
10158                visible: "fg-visible",
10159                reverse: "fg-reverse",
10160                toggled: "fg-toggled",
10161                selected: "fg-selected",
10162                disabled: "fg-disabled",
10163                hidden: "fg-hidden",
10164                started: "fg-started",
10165                stopped: "fg-stopped",
10166                paused: "fg-paused",
10167                noTransitions: "fg-no-transitions"
10168            },
10169
10170            buttons: {
10171                portrait: {
10172                    top: "fg-panel-buttons-top",
10173                    bottom: "fg-panel-buttons-bottom"
10174                },
10175                landscape: {
10176                    right: "fg-panel-buttons-right",
10177                    left: "fg-panel-buttons-left"
10178                },
10179                container: "fg-panel-buttons",
10180                prev: "fg-panel-button fg-panel-button-prev",
10181                next: "fg-panel-button fg-panel-button-next",
10182                autoProgress: "fg-panel-button fg-panel-button-progress",
10183                close: "fg-panel-button fg-panel-button-close",
10184                fullscreen: "fg-panel-button fg-panel-button-fullscreen",
10185                maximize: "fg-panel-button fg-panel-button-maximize",
10186                info: "fg-panel-button fg-panel-button-info",
10187                thumbs: "fg-panel-button fg-panel-button-thumbs",
10188                cart: "fg-panel-button fg-panel-button-cart",
10189                download: "fg-panel-button fg-panel-button-download"
10190            },
10191
10192            transition: {
10193                fade: "fg-panel-fade",
10194                horizontal: "fg-panel-horizontal",
10195                vertical: "fg-panel-vertical"
10196            },
10197
10198            area: {
10199                elem: "fg-panel-area",
10200                inner: "fg-panel-area-inner"
10201            },
10202
10203            content: {
10204                buttons: "fg-panel-content-buttons"
10205            },
10206
10207            sideArea: {
10208                toggle: "fg-panel-area-toggle",
10209                button: "fg-panel-area-button",
10210                visible: "fg-panel-area-visible",
10211                overlay: "fg-panel-area-overlay",
10212                position: {
10213                    top: "fg-panel-area-top",
10214                    right: "fg-panel-area-right",
10215                    bottom: "fg-panel-area-bottom",
10216                    left: "fg-panel-area-left"
10217                }
10218            },
10219
10220            info: {
10221                overlay: "fg-panel-info-overlay",
10222                align: {
10223                    left: "fg-panel-media-caption-left",
10224                    center: "fg-panel-media-caption-center",
10225                    right: "fg-panel-media-caption-right",
10226                    justified: "fg-panel-media-caption-justified"
10227                }
10228            },
10229
10230            cart: {
10231                original: "fg-panel-cart-original"
10232            },
10233
10234            thumbs: {
10235                prev: "fg-panel-thumbs-button fg-panel-thumbs-prev",
10236                next: "fg-panel-thumbs-button fg-panel-thumbs-next",
10237                viewport: "fg-panel-thumbs-viewport",
10238                stage: "fg-panel-thumbs-stage",
10239                noCaptions: "fg-panel-thumbs-no-captions",
10240                small: "fg-panel-thumbs-small",
10241                spacer: "fg-panel-thumb-spacer",
10242                thumb: {
10243                    elem: "fg-panel-thumb",
10244                    media: "fg-panel-thumb-media",
10245                    overlay: "fg-panel-thumb-overlay",
10246                    wrap: "fg-panel-thumb-wrap",
10247                    image: "fg-panel-thumb-image",
10248                    caption: "fg-panel-thumb-caption",
10249                    title: "fg-panel-thumb-title",
10250                    description: "fg-panel-thumb-description"
10251                },
10252                align: {
10253                    left: "fg-panel-thumb-caption-left",
10254                    center: "fg-panel-thumb-caption-center",
10255                    right: "fg-panel-thumb-caption-right",
10256                    justified: "fg-panel-thumb-caption-justified"
10257                }
10258            }
10259        }
10260    },{
10261        panel: {
10262            buttons: {
10263                prev: "Previous Media",
10264                next: "Next Media",
10265                close: "Close Modal",
10266                maximize: "Toggle Maximize",
10267                fullscreen: "Toggle Fullscreen",
10268                autoProgress: "Auto Progress",
10269                info: "Toggle Information",
10270                thumbs: "Toggle Thumbnails",
10271                cart: "Toggle Cart",
10272                download: "Download Media"
10273            }
10274        }
10275    });
10276
10277})(
10278    FooGallery.$,
10279    FooGallery,
10280    FooGallery.utils,
10281    FooGallery.utils.is,
10282    FooGallery.utils.obj,
10283    FooGallery.utils.fn,
10284    FooGallery.utils.transition
10285);
10286
10287(function($, _, _icons, _utils, _is){
10288
10289    _.Panel.Buttons = _utils.Class.extend({
10290        construct: function(panel){
10291            this.panel = panel;
10292
10293            this.opt = panel.opt.buttons;
10294
10295            this.cls = panel.cls.buttons;
10296
10297            this.sel = panel.sel.buttons;
10298
10299            this.il8n = panel.il8n.buttons;
10300
10301            this.$el = null;
10302
10303            this.isCreated = false;
10304
10305            this.isAttached = false;
10306
10307            this.__registered = [];
10308
10309            this.registerCore();
10310        },
10311
10312        registerCore: function(){
10313            this.register(new _.Panel.Button(this.panel, "prev", {
10314                icon: "arrow-left",
10315                label: this.il8n.prev,
10316                click: this.panel.prev.bind(this.panel),
10317                beforeLoad: function (media) {
10318                    this.disable(this.panel.prevItem == null);
10319                }
10320            }), 10);
10321            this.register(new _.Panel.Button(this.panel, "next", {
10322                icon: "arrow-right",
10323                label: this.il8n.next,
10324                click: this.panel.next.bind(this.panel),
10325                beforeLoad: function (media) {
10326                    this.disable(this.panel.nextItem == null);
10327                }
10328            }), 20);
10329            this.register(new _.Panel.AutoProgress(this.panel), 30);
10330
10331            // area buttons are inserted by default with priority 99
10332
10333            this.register(new _.Panel.Download(this.panel), 170);
10334            this.register(new _.Panel.Maximize(this.panel), 180);
10335            this.register(new _.Panel.Fullscreen(this.panel), 190);
10336            this.register(new _.Panel.Button(this.panel, "close", {
10337                icon: "close",
10338                label: this.il8n.close,
10339                click: this.panel.close.bind(this.panel)
10340            }), 200);
10341        },
10342
10343        register: function( button, priority ){
10344            if (button instanceof _.Panel.Button){
10345                return this.__registered.push({
10346                    name: button.name,
10347                    button: button,
10348                    priority: _is.number(priority) ? priority : 99
10349                }) - 1;
10350            }
10351            return -1;
10352        },
10353
10354        get: function( name ){
10355            var button = null;
10356            for (var i = 0, l = this.__registered.length; i < l; i++){
10357                if (this.__registered[i].name !== name) continue;
10358                button = this.__registered[i].button;
10359                break;
10360            }
10361            return button;
10362        },
10363
10364        each: function(callback, prioritize){
10365            var self = this;
10366            if (prioritize){
10367                self.__registered.sort(function(a, b){
10368                    return b.priority - a.priority;
10369                });
10370            }
10371            self.__registered.forEach(function(registered, index){
10372                callback.call(self, registered.button, index);
10373            });
10374        },
10375
10376        toggle: function( name, visible ){
10377            var button = this.get(name);
10378            if (button == null) return;
10379            button.toggle(visible);
10380        },
10381
10382        disable: function( name, disable ){
10383            var button = this.get(name);
10384            if (button == null) return;
10385            button.disable(disable);
10386        },
10387
10388        press: function( name, pressed ){
10389            var self = this, button = self.get(name);
10390            if ( pressed && _is.string(button.groupName) ){
10391                self.each(function(btn){
10392                    if ( button !== btn && btn instanceof _.Panel.SideAreaButton && button.groupName === btn.groupName ){
10393                        if ( self.panel.isSmallScreen || btn.isTargetingSamePosition( button ) ) {
10394                            btn.area.toggle( false );
10395                        }
10396                    }
10397                });
10398            }
10399            button.press( pressed );
10400        },
10401
10402        destroy: function(){
10403            var self = this;
10404            var e = self.panel.trigger("buttons-destroy", [self]);
10405            if (!e.isDefaultPrevented()) {
10406                self.isCreated = !self.doDestroy();
10407            }
10408            if (!self.isCreated) {
10409                self.panel.trigger("buttons-destroyed", [self]);
10410            }
10411            return !self.isCreated;
10412        },
10413        doDestroy: function(){
10414            var self = this;
10415            self.each(function(button){
10416                button.destroy();
10417            });
10418            if (self.isCreated){
10419                self.detach();
10420                self.$el.remove();
10421            }
10422            return true;
10423        },
10424        create: function(){
10425            var self = this;
10426            if (!self.isCreated) {
10427                var e = self.panel.trigger("buttons-create", [self]);
10428                if (!e.isDefaultPrevented()) {
10429                    self.isCreated = self.doCreate();
10430                }
10431                if (self.isCreated) {
10432                    self.panel.trigger("buttons-created", [self]);
10433                }
10434            }
10435            return self.isCreated;
10436        },
10437        doCreate: function(){
10438            var self = this;
10439            self.$el = $('<div/>').addClass(self.cls.container);
10440
10441            self.each(function(button){
10442                button.appendTo(self.$el);
10443            }, true);
10444
10445            return true;
10446        },
10447        appendTo: function( parent ){
10448            var self = this;
10449            if (!self.isCreated){
10450                self.create();
10451            }
10452            if (self.isCreated && !self.isAttached){
10453                var e = self.panel.trigger("buttons-append", [self, parent]);
10454                if (!e.isDefaultPrevented()) {
10455                    self.isAttached = self.doAppendTo( parent );
10456                }
10457                if (self.isAttached) {
10458                    self.panel.trigger("buttons-appended", [self, parent]);
10459                }
10460            }
10461            return self.isAttached;
10462        },
10463        doAppendTo: function( parent ){
10464            this.$el.appendTo( parent );
10465            return this.$el.parent().length > 0;
10466        },
10467        detach: function(){
10468            var self = this;
10469            if (self.isCreated && self.isAttached) {
10470                var e = self.panel.trigger("buttons-detach", [self]);
10471                if (!e.isDefaultPrevented()) {
10472                    self.isAttached = !self.doDetach();
10473                }
10474                if (!self.isAttached) {
10475                    self.panel.trigger("buttons-detached", [self]);
10476                }
10477            }
10478            return !self.isAttached;
10479        },
10480        doDetach: function(){
10481            this.$el.detach();
10482            return true;
10483        },
10484
10485        beforeLoad: function(media){
10486            this.each(function(button){
10487                button.beforeLoad(media);
10488            });
10489        },
10490
10491        afterLoad: function(media){
10492            this.each(function(button){
10493                button.afterLoad(media);
10494            });
10495        },
10496
10497        close: function(){
10498            this.each(function(button){
10499                button.close();
10500            });
10501        },
10502
10503        resize: function(){
10504            const prev = this.get("prev");
10505            const next = this.get("next");
10506            const target = this.panel.isMobileLayout ? this.$el : this.panel.content.$buttons;
10507            if ( next ) next.appendTo( target );
10508            if ( prev ) prev.appendTo( target );
10509            this.each(function(button){
10510                button.resize();
10511            });
10512        }
10513    });
10514
10515})(
10516    FooGallery.$,
10517    FooGallery,
10518    FooGallery.icons,
10519    FooGallery.utils,
10520    FooGallery.utils.is
10521);
10522(function($, _, _icons, _utils, _is, _obj){
10523
10524    _.Panel.Button = _utils.Class.extend({
10525        construct: function(panel, name, options){
10526            this.panel = panel;
10527            this.name = name;
10528            this.opt = _obj.extend({
10529                icon: null,
10530                label: null,
10531                visible: !!panel.opt.buttons[name],
10532                disabled: false,
10533                toggle: false,
10534                pressed: false,
10535                group: null,
10536                click: $.noop,
10537                beforeLoad: $.noop,
10538                afterLoad: $.noop,
10539                close: $.noop,
10540                resize: $.noop
10541            }, options);
10542            this.cls = {
10543                elem: panel.cls.buttons[name],
10544                states: panel.cls.states
10545            };
10546            this.$el = null;
10547            this.groupName = this.opt.group;
10548            this.isVisible = this.opt.visible;
10549            this.isDisabled = this.opt.disabled;
10550            this.isToggle = this.opt.toggle;
10551            this.isPressed = this.opt.pressed;
10552            this.isCreated = false;
10553            this.isAttached = false;
10554        },
10555        isEnabled: function(){
10556            return this.panel.opt.buttons.hasOwnProperty(this.name) && this.panel.opt.buttons[this.name];
10557        },
10558        create: function(){
10559            var self = this;
10560            if (!self.isCreated){
10561                self.$el = $('<button/>', {
10562                    type: 'button',
10563                    "aria-label": self.opt.label,
10564                    "aria-disabled": self.isDisabled,
10565                    "aria-hidden": !self.isVisible
10566                }).addClass(self.cls.elem).on("click.foogallery", {self: self}, self.onButtonClick);
10567                if (_is.string(self.opt.icon)){
10568                    self.$el.append(_icons.get(self.opt.icon, self.panel.opt.icons));
10569                } else if (_is.array(self.opt.icon)){
10570                    self.opt.icon.forEach(function(icon){
10571                        self.$el.append(_icons.get(icon, self.panel.opt.icons));
10572                    });
10573                } else if (_is.fn(self.opt.icon)){
10574                    self.$el.append(self.opt.icon.call(this));
10575                }
10576                self.isCreated = true;
10577                var enabled = self.isEnabled();
10578                self.toggle(enabled);
10579                self.disable(!enabled);
10580                if (self.isToggle) self.press( self.isPressed );
10581            }
10582            return self.isCreated;
10583        },
10584        destroy: function(){
10585            if (this.isCreated){
10586                this.$el.off("click.foogallery").remove();
10587                this.isCreated = false;
10588            }
10589            return !this.isCreated;
10590        },
10591        appendTo: function(parent){
10592            if ( this.isAttached ) {
10593                this.detach();
10594            }
10595            if ((this.isCreated || this.create()) && !this.isAttached){
10596                this.$el.appendTo(parent);
10597                this.isAttached = true;
10598            }
10599            return this.isAttached;
10600        },
10601        prependTo: function(parent){
10602            if ( this.isAttached ) {
10603                this.detach();
10604            }
10605            if ((this.isCreated || this.create()) && !this.isAttached){
10606                this.$el.prependTo(parent);
10607                this.isAttached = true;
10608            }
10609            return this.isAttached;
10610        },
10611        detach: function(){
10612            if (this.isCreated && this.isAttached){
10613                this.$el.detach();
10614                this.isAttached = false;
10615            }
10616            return this.isAttached;
10617        },
10618        toggle: function(visible){
10619            if (!this.isCreated) return;
10620            this.isVisible = _is.boolean(visible) ? visible : !this.isVisible;
10621            this.$el.toggleClass(this.cls.states.hidden, !this.isVisible).attr("aria-hidden", !this.isVisible);
10622        },
10623        disable: function(disabled){
10624            if (!this.isCreated) return;
10625            this.isDisabled = _is.boolean(disabled) ? disabled : !this.isDisabled;
10626            this.$el.toggleClass(this.cls.states.disabled, this.isDisabled)
10627                .attr("aria-disabled", this.isDisabled)
10628                .prop("disabled", this.isDisabled);
10629        },
10630        press: function(pressed){
10631            if ( !this.isCreated ) return;
10632            this.isPressed = pressed;
10633            this.$el.attr("aria-pressed", this.isPressed);
10634        },
10635        beforeLoad: function(media){
10636            this.opt.beforeLoad.call(this, media);
10637        },
10638        afterLoad: function(media){
10639            this.opt.afterLoad.call(this, media);
10640        },
10641        close: function(){
10642            this.opt.close.call(this);
10643        },
10644        click: function(){
10645            this.opt.click.call(this);
10646        },
10647        resize: function(){
10648            this.opt.resize.call(this);
10649        },
10650        onButtonClick: function (e) {
10651            e.preventDefault();
10652            e.data.self.click();
10653        }
10654    });
10655
10656})(
10657    FooGallery.$,
10658    FooGallery,
10659    FooGallery.icons,
10660    FooGallery.utils,
10661    FooGallery.utils.is,
10662    FooGallery.utils.obj
10663);
10664(function($, _, _is){
10665
10666    _.Panel.SideAreaButton = _.Panel.Button.extend({
10667        construct: function(area){
10668            this._super(area.panel, area.name, {
10669                icon: area.opt.icon,
10670                label: area.opt.label,
10671                autoHideArea: area.opt.autoHide,
10672                click: area.toggle.bind(area),
10673                toggle: true,
10674                pressed: area.opt.visible,
10675                group: area.opt.group
10676            });
10677            this.area = area;
10678            this.__isVisible = null;
10679            this.__autoHide = null;
10680        },
10681        beforeLoad: function(media){
10682            var enabled = this.area.isEnabled(), supported = enabled && this.area.canLoad(media);
10683            if (!supported && this.__isVisible == null){
10684                this.__isVisible = this.area.isVisible;
10685                this.area.toggle(false);
10686            } else if (supported && _is.boolean(this.__isVisible)) {
10687                this.area.toggle(this.__isVisible);
10688                this.__isVisible = null;
10689            }
10690            if (enabled) this.disable(!supported);
10691            else this.toggle(supported);
10692
10693            this.checkAutoHide(enabled, supported);
10694
10695            this.opt.beforeLoad.call(this, media);
10696        },
10697        checkAutoHide: function(enabled, supported){
10698            if (enabled && supported && this.opt.autoHideArea === true){
10699                if (this.__autoHide == null && this.panel.isSmallScreen) {
10700                    this.__autoHide = this.area.isVisible;
10701                    this.area.toggle(false);
10702                    this.area.button.toggle(true);
10703                } else if (_is.boolean(this.__autoHide) && !this.panel.isSmallScreen) {
10704                    this.area.button.toggle(this.area.button.isEnabled() && this.area.opt.toggle);
10705                    this.area.toggle(this.__autoHide);
10706                    this.__autoHide = null;
10707                }
10708            }
10709        },
10710        isTargetingSamePosition: function( button ) {
10711            if ( button instanceof _.Panel.SideAreaButton ) {
10712                return button.area.isTargetingSamePosition( this.area );
10713            }
10714            return false;
10715        },
10716        resize: function(){
10717            var enabled = this.area.isEnabled(), supported = enabled && this.area.canLoad(this.area.currentMedia);
10718            this.checkAutoHide(enabled, supported);
10719        }
10720    });
10721
10722})(
10723    FooGallery.$,
10724    FooGallery,
10725    FooGallery.utils.is
10726);
10727(function($, _, _utils){
10728
10729    _.Panel.AutoProgress = _.Panel.Button.extend({
10730        construct: function(panel){
10731            var self = this;
10732            self.__stopped = !panel.opt.autoProgressStart;
10733            self.__timer = new _utils.Timer();
10734            self._super(panel, "autoProgress", {
10735                icon: "auto-progress",
10736                label: panel.il8n.buttons.autoProgress
10737            });
10738            self.$icon = null;
10739            self.$circle = null;
10740            self.circumference = 0;
10741        },
10742        isEnabled: function(){
10743            return this._super() && this.panel.opt.autoProgress > 0;
10744        },
10745        create: function () {
10746            if (this._super()){
10747                if (this.panel.opt.autoProgressVisible === false){
10748                    this.toggle(false);
10749                }
10750                this.$icon = this.$el.find("svg");
10751                this.$circle = this.$icon.find("circle");
10752                var radius = parseFloat(this.$circle.attr("r"));
10753                this.circumference = (radius * 2) * Math.PI;
10754                this.$circle.css({
10755                    "stroke-dasharray": this.circumference + ' ' + this.circumference,
10756                    "stroke-dashoffset": this.circumference + ''
10757                });
10758                this.__timer.on({
10759                    "start resume": this.onStartOrResume,
10760                    "pause": this.onPause,
10761                    "stop": this.onStop,
10762                    "tick": this.onTick,
10763                    "complete reset": this.onCompleteOrReset,
10764                    "complete": this.onComplete
10765                }, this);
10766            }
10767            return this.isCreated;
10768        },
10769        close: function(){
10770            this.__timer.pause();
10771            this._super();
10772        },
10773        destroy: function(){
10774            this.__timer.destroy();
10775            return this._super();
10776        },
10777        beforeLoad: function(media){
10778            if (this.isEnabled()) {
10779                this.__timer.reset();
10780            }
10781            this._super(media);
10782        },
10783        afterLoad: function(media){
10784            if (this.isEnabled()) {
10785                this.__timer.countdown(this.panel.opt.autoProgress);
10786                if (this.__stopped) this.__timer.pause();
10787            }
10788            this._super(media);
10789        },
10790        click: function(){
10791            if (this.__timer.isRunning){
10792                this.__stopped = true;
10793                this.__timer.pause();
10794            } else if (this.__timer.canResume) {
10795                this.__stopped = false;
10796                this.__timer.resume();
10797            } else {
10798                this.__stopped = false;
10799                this.__timer.restart();
10800            }
10801            this._super();
10802        },
10803        onStartOrResume: function(){
10804            this.$icon.removeClass(this.cls.states.allProgress).addClass(this.cls.states.started);
10805        },
10806        onPause: function(){
10807            this.$icon.removeClass(this.cls.states.allProgress).addClass(this.cls.states.paused);
10808        },
10809        onStop: function(){
10810            this.$icon.removeClass(this.cls.states.allProgress).addClass(this.cls.states.stopped);
10811        },
10812        onTick: function(e, current, time){
10813            var percent = current / time * 100;
10814            this.$circle.css("stroke-dashoffset", this.circumference - percent / 100 * this.circumference);
10815        },
10816        onCompleteOrReset: function(){
10817            this.$icon.removeClass(this.cls.states.allProgress);
10818        },
10819        onComplete: function(){
10820            this.panel.next();
10821        }
10822    });
10823
10824})(
10825    FooGallery.$,
10826    FooGallery,
10827    FooGallery.utils
10828);
10829
10830(function($, _, _utils){
10831
10832    _.fullscreen = new _utils.FullscreenAPI();
10833
10834    _.Panel.Fullscreen = _.Panel.Button.extend({
10835        construct: function(panel){
10836            var self = this;
10837            self._super(panel, "fullscreen", {
10838                icon: ["expand", "shrink"],
10839                label: panel.il8n.buttons.fullscreen,
10840                toggle: true
10841            });
10842        },
10843        isEnabled: function(){
10844            return this._super() && _.fullscreen.supported;
10845        },
10846        click: function(){
10847            var self = this, pnl = self.panel.el;
10848            _.fullscreen.toggle(pnl).then(function(){
10849                if (_.fullscreen.element() === pnl){
10850                    _.fullscreen.on("change error", self.onFullscreenChange, self);
10851                    self.enter();
10852                } else {
10853                    _.fullscreen.off("change error", self.onFullscreenChange, self);
10854                    self.exit();
10855                }
10856            }, function(err){
10857                console.debug('Error toggling fullscreen on element.', err, pnl);
10858            });
10859            self._super();
10860        },
10861        onFullscreenChange: function(){
10862            if (_.fullscreen.element() !== this.panel.el){
10863                this.exit();
10864            }
10865        },
10866        enter: function(){
10867            if (this.panel.isFullscreen) return;
10868            this.panel.isFullscreen = true;
10869            this.panel.$el.addClass(this.panel.cls.fullscreen);
10870            if (!this.panel.isMaximized){
10871                this.panel.$el.attr({
10872                    'role': 'dialog',
10873                    'aria-modal': true
10874                }).trigger('focus');
10875                this.panel.trapFocus();
10876            }
10877            this.panel.buttons.press('fullscreen', true);
10878            this.panel.buttons.toggle('maximize', false);
10879        },
10880        exit: function(){
10881            if (!this.panel.isFullscreen) return;
10882            this.panel.$el.removeClass(this.panel.cls.fullscreen);
10883            if (!this.panel.isMaximized){
10884                this.panel.$el.attr({
10885                    'role': null,
10886                    'aria-modal': null
10887                }).trigger('focus');
10888                this.panel.releaseFocus();
10889            }
10890            this.panel.buttons.press('fullscreen', false);
10891            this.panel.buttons.toggle('maximize', this.panel.isInline && this.panel.buttons.opt.maximize);
10892            this.panel.isFullscreen = false;
10893        }
10894    });
10895
10896})(
10897    FooGallery.$,
10898    FooGallery,
10899    FooGallery.utils
10900);
10901(function($, _, _is){
10902
10903    var root = document.documentElement;
10904    var scrollKeys = {
10905        " ": true,
10906        ArrowDown: true,
10907        ArrowLeft: true,
10908        ArrowRight: true,
10909        ArrowUp: true,
10910        End: true,
10911        Home: true,
10912        PageDown: true,
10913        PageUp: true
10914    };
10915
10916    function getElement( target ) {
10917        if (target && target.nodeType === 3) return target.parentElement;
10918        return target && target.nodeType === 1 ? target : null;
10919    }
10920
10921    function hasScrollableAncestor( target, panel ) {
10922        var element = getElement(target), style;
10923        while (element && panel.contains(element)) {
10924            style = window.getComputedStyle(element);
10925            if (
10926                (/(auto|scroll|overlay)/.test(style.overflowY) && element.scrollHeight > element.clientHeight) ||
10927                (/(auto|scroll|overlay)/.test(style.overflowX) && element.scrollWidth > element.clientWidth)
10928            ) {
10929                return true;
10930            }
10931            if (element === panel) break;
10932            element = element.parentElement;
10933        }
10934        return false;
10935    }
10936
10937    function isEditable( target ) {
10938        return target && (
10939            target.isContentEditable ||
10940            (target.tagName && /^(INPUT|TEXTAREA|SELECT|BUTTON|A)$/.test( target.tagName.toUpperCase() ))
10941        );
10942    }
10943
10944    _.Panel.Maximize = _.Panel.Button.extend({
10945        construct: function(panel){
10946            this._super(panel, "maximize", {
10947                icon: "maximize",
10948                label: panel.il8n.buttons.maximize,
10949                toggle: true
10950            });
10951            this._scrollState = null;
10952            this.$placeholder = $("<span/>");
10953        },
10954        click: function(){
10955            this.set(!this.panel.isMaximized);
10956            this._super();
10957        },
10958        close: function(){
10959            this.exit();
10960            this._super();
10961        },
10962        set: function(maximized, visible){
10963            if (maximized) this.enter();
10964            else this.exit();
10965            visible = _is.boolean(visible) ? visible : this.isVisible;
10966            this.toggle(visible);
10967        },
10968        lockScroll: function(){
10969            var body = document.body;
10970            if (!body || this._scrollState) return;
10971
10972            var pageHeight = Math.max(root.scrollHeight, body.scrollHeight, root.clientHeight);
10973            var panel = this.panel.$el.get(0);
10974            var state = this._scrollState = {
10975                x: window.scrollX,
10976                y: window.scrollY,
10977                rootMinHeight: root.style.minHeight,
10978                bodyPosition: body.style.position,
10979                bodyTop: body.style.top,
10980                bodyLeft: body.style.left,
10981                bodyRight: body.style.right,
10982                bodyWidth: body.style.width,
10983                preventScroll: function(event){
10984                    if (!hasScrollableAncestor(event.target, panel)) event.preventDefault();
10985                },
10986                preventKey: function(event){
10987                    if (
10988                        scrollKeys[event.key] &&
10989                        !isEditable(event.target) &&
10990                        !hasScrollableAncestor(event.target, panel)
10991                    ) {
10992                        event.preventDefault();
10993                    }
10994                }
10995            };
10996
10997            root.classList.add(this.panel.cls.noScrollbars);
10998            root.style.minHeight = pageHeight + "px";
10999            body.style.position = "fixed";
11000            body.style.top = -state.y + "px";
11001            body.style.left = -state.x + "px";
11002            body.style.right = "0";
11003            body.style.width = "100%";
11004
11005            document.addEventListener("wheel", state.preventScroll, {capture: true, passive: false});
11006            document.addEventListener("touchmove", state.preventScroll, {capture: true, passive: false});
11007            document.addEventListener("keydown", state.preventKey, true);
11008        },
11009        unlockScroll: function(){
11010            var state = this._scrollState, body = document.body;
11011            if (!state || !body) return;
11012
11013            document.removeEventListener("wheel", state.preventScroll, {capture: true});
11014            document.removeEventListener("touchmove", state.preventScroll, {capture: true});
11015            document.removeEventListener("keydown", state.preventKey, true);
11016
11017            root.classList.remove(this.panel.cls.noScrollbars);
11018            root.style.minHeight = state.rootMinHeight;
11019            body.style.position = state.bodyPosition;
11020            body.style.top = state.bodyTop;
11021            body.style.left = state.bodyLeft;
11022            body.style.right = state.bodyRight;
11023            body.style.width = state.bodyWidth;
11024
11025            this._scrollState = null;
11026            window.scrollTo(state.x, state.y);
11027        },
11028        enter: function(){
11029            if (this.panel.isMaximized) return;
11030            this.panel.isMaximized = true;
11031            this.$placeholder.insertAfter(this.panel.$el);
11032            this.panel.$el.appendTo("body").addClass(this.panel.cls.maximized).attr({
11033                'role': 'dialog',
11034                'aria-modal': true
11035            }).trigger('focus');
11036            this.panel.buttons.press('maximize', true);
11037            this.panel.trapFocus();
11038            if (this.panel.opt.noScrollbars){
11039                this.lockScroll();
11040            }
11041        },
11042        exit: function(){
11043            if (!this.panel.isMaximized) return;
11044            this.panel.isMaximized = false;
11045            this.panel.$el.removeClass(this.panel.cls.maximized).attr({
11046                'role': null,
11047                'aria-modal': null
11048            }).insertBefore(this.$placeholder);
11049            if (this.panel.isInline) this.panel.$el.trigger('focus');
11050            this.$placeholder.detach();
11051            this.panel.buttons.press('maximize', false);
11052            this.panel.releaseFocus();
11053            if (this.panel.opt.noScrollbars){
11054                this.unlockScroll();
11055            }
11056        }
11057    });
11058
11059})(
11060    FooGallery.$,
11061    FooGallery,
11062    FooGallery.utils.is
11063);
11064
11065(function($, _, _is){
11066
11067    _.Panel.Download = _.Panel.Button.extend({
11068        construct: function(panel){
11069            this._super(panel, "download", {
11070                icon: "download",
11071                label: panel.il8n.buttons.download,
11072                toggle: false
11073            });
11074            this.downloadable = [ 'image' ];
11075        },
11076        beforeLoad: function(media) {
11077            this._super( media );
11078            if ( this.isEnabled() ) {
11079                this.toggle( this.downloadable.includes( media?.item?.type ) );
11080            }
11081        },
11082        click: function(){
11083            this._super();
11084            const i = this.panel.currentItem;
11085            if ( i instanceof _.Item && _is.string( i.download ) ) {
11086                this.disable( true );
11087                _.downloadImage( i.download )
11088                    .catch( err => console.error( err ) )
11089                    .finally( () => this.disable( false ) );
11090            }
11091        }
11092    });
11093
11094})(
11095    FooGallery.$,
11096    FooGallery,
11097    FooGallery.utils.is
11098);
11099(function($, _, _utils, _is, _fn, _obj, _str){
11100
11101    /**
11102     * @memberof FooGallery.Panel.
11103     * @class Area
11104     * @augments FooGallery.utils.Class
11105     */
11106    _.Panel.Area = _utils.Class.extend(/** @lends FooGallery.Panel.Area */{
11107        /**
11108         * @ignore
11109         * @constructs
11110         * @param panel
11111         * @param name
11112         * @param options
11113         * @param classes
11114         */
11115        construct: function(panel, name, options, classes){
11116            this.panel = panel;
11117            this.name = name;
11118            this.opt = _obj.extend({
11119                waitForUnload: true
11120            }, options);
11121            this.cls = _obj.extend({
11122                elem: this.__cls(panel.cls.area.elem, name, true),
11123                inner: this.__cls(panel.cls.area.inner, name, true)
11124            }, classes);
11125            this.sel = _utils.selectify(this.cls);
11126            this.currentMedia = null;
11127            this.$el = null;
11128            this.$inner = null;
11129            this.isCreated = false;
11130            this.isAttached = false;
11131        },
11132        __cls: function(cls, replacement, andOriginal){
11133            var formatted = cls.replace(/-area($|-)/, "-" + replacement + "$1");
11134            return andOriginal ? [ cls, formatted ].join(" ") : formatted;
11135        },
11136        create: function(){
11137            var self = this;
11138            if (!self.isCreated) {
11139                var e = self.panel.trigger("area-create", [self]);
11140                if (!e.isDefaultPrevented()) {
11141                    self.isCreated = self.doCreate();
11142                }
11143                if (self.isCreated) {
11144                    self.panel.trigger("area-created", [self]);
11145                }
11146            }
11147            return self.isCreated;
11148        },
11149        doCreate: function(){
11150            this.$el = $("<div/>").addClass(this.cls.elem);
11151            this.$inner = $("<div/>").addClass(this.cls.inner).appendTo(this.$el);
11152            return true;
11153        },
11154        destroy: function(){
11155            var self = this;
11156            if (self.isCreated){
11157                var e = self.panel.trigger("area-destroy", [self]);
11158                if (!e.isDefaultPrevented()) {
11159                    self.isCreated = !self.doDestroy();
11160                }
11161                if (!self.isCreated) {
11162                    self.panel.trigger("area-destroyed", [self]);
11163                }
11164            }
11165            return !self.isCreated;
11166        },
11167        doDestroy: function(){
11168            if (this.currentMedia instanceof _.Panel.Media){
11169                this.currentMedia.detach();
11170            }
11171            this.$el.remove();
11172            return true;
11173        },
11174        appendTo: function( parent ){
11175            var self = this;
11176            if (!self.isCreated){
11177                self.create();
11178            }
11179            if (self.isCreated && !self.isAttached){
11180                var e = self.panel.trigger("area-append", [self, parent]);
11181                if (!e.isDefaultPrevented()) {
11182                    self.isAttached = self.doAppendTo( parent );
11183                }
11184                if (self.isAttached) {
11185                    self.panel.trigger("area-appended", [self, parent]);
11186                }
11187            }
11188            return self.isAttached;
11189        },
11190        doAppendTo: function( parent ){
11191            this.$el.appendTo( parent );
11192            return this.$el.parent().length > 0;
11193        },
11194        detach: function(){
11195            var self = this;
11196            if (self.isCreated && self.isAttached) {
11197                var e = self.panel.trigger("area-detach", [self]);
11198                if (!e.isDefaultPrevented()) {
11199                    self.isAttached = !self.doDetach();
11200                }
11201                if (!self.isAttached) {
11202                    self.panel.trigger("area-detached", [self]);
11203                }
11204            }
11205            return !self.isAttached;
11206        },
11207        doDetach: function(){
11208            this.$el.detach();
11209            return true;
11210        },
11211        load: function(media){
11212            var self = this;
11213            if (!(media instanceof _.Panel.Media)) return _fn.reject("unable to load media");
11214            return $.Deferred(function(def){
11215                var reverseTransition = self.shouldReverseTransition(self.currentMedia, media);
11216                var e = self.panel.trigger("area-load", [self, media, reverseTransition]);
11217                if (e.isDefaultPrevented()){
11218                    def.rejectWith("default prevented");
11219                    return;
11220                }
11221                var hasMedia = self.currentMedia instanceof _.Panel.Media, prev = self.currentMedia;
11222                if (self.opt.waitForUnload && hasMedia){
11223                    self.panel.trigger("area-unload", [self, prev]);
11224                    self.doUnload(prev, reverseTransition).then(function(){
11225                        self.panel.trigger("area-unloaded", [self, prev]);
11226                        self.currentMedia = media;
11227                        self.panel.trigger("area-load", [self, media]);
11228                        self.doLoad(media, reverseTransition).then(def.resolve).fail(def.reject);
11229                    }).fail(def.reject);
11230                } else {
11231                    if (hasMedia){
11232                        self.panel.trigger("area-unload", [self, prev]);
11233                        self.doUnload(prev, reverseTransition).then(function(){
11234                            self.panel.trigger("area-unloaded", [self, prev]);
11235                        });
11236                    }
11237                    self.currentMedia = media;
11238                    self.panel.trigger("area-load", [self, media]);
11239                    self.doLoad(media, reverseTransition).then(def.resolve).fail(def.reject);
11240                }
11241            }).then(function(){
11242                self.panel.trigger("area-loaded", [self, media]);
11243            }).fail(function(){
11244                self.panel.trigger("area-error", [self, media]);
11245            }).promise();
11246        },
11247        doLoad: function(media, reverseTransition){
11248            return _fn.resolved;
11249        },
11250        doUnload: function(media, reverseTransition){
11251            return _fn.resolved;
11252        },
11253        close: function(immediate){
11254            var self = this;
11255            if (self.currentMedia instanceof _.Panel.Media){
11256                var current = self.currentMedia;
11257                if (!immediate){
11258                    self.panel.trigger("area-unload", [self, current]);
11259                    return self.doUnload(current, false).then(function() {
11260                        self.panel.trigger("area-unloaded", [self, current]);
11261                    }).always(function(){
11262                        self.currentMedia = null;
11263                    });
11264                }
11265                self.panel.trigger("area-unload", [self, current]);
11266                self.doUnload(current, false).then(function(){
11267                    self.panel.trigger("area-unloaded", [self, current]);
11268                });
11269                self.currentMedia = null;
11270            }
11271            return _fn.resolved;
11272        },
11273        shouldReverseTransition: function( oldMedia, newMedia ){
11274            if (!(oldMedia instanceof _.Panel.Media) || !(newMedia instanceof _.Panel.Media)) return true;
11275            var result = oldMedia.item.index < newMedia.item.index,
11276                last = this.panel.tmpl.items.last(this.panel.isVisible);
11277            if (last instanceof _.Item && ((newMedia.item.index === 0 && oldMedia.item.index === last.index) || (newMedia.item.index === last.index && oldMedia.item.index === 0))){
11278                result = !result;
11279            }
11280            return result;
11281        },
11282        listen: function(){},
11283        stopListening: function(){},
11284        resize: function(){}
11285    });
11286
11287})(
11288    FooGallery.$,
11289    FooGallery,
11290    FooGallery.utils,
11291    FooGallery.utils.is,
11292    FooGallery.utils.fn,
11293    FooGallery.utils.obj,
11294    FooGallery.utils.str
11295);
11296
11297(function($, _, _fn, _t){
11298
11299    /**
11300     * @memberof FooGallery.Panel.
11301     * @class Content
11302     * @augments FooGallery.Panel.Area
11303     */
11304    _.Panel.Content = _.Panel.Area.extend(/** @lends FooGallery.Panel.Content */{
11305        /**
11306         * @ignore
11307         * @constructs
11308         * @param panel
11309         */
11310        construct: function(panel){
11311            this._super(panel, "content", {
11312                waitForUnload: false
11313            }, panel.cls.content);
11314            this.$buttons = null;
11315            this.robserver = null;
11316        },
11317        doCreate: function(){
11318            var self = this;
11319            if (self._super()){
11320                self.$buttons = $( "<div/>" ).addClass( self.cls.buttons ).appendTo( self.$el );
11321                if (self.panel.opt.swipe){
11322                    self.$inner.fgswipe({data: {self: self}, swipe: self.onSwipe, allowPageScroll: true});
11323                }
11324                self.robserver = new ResizeObserver(_fn.throttle(function () {
11325                    if (self.panel instanceof _.Panel && !self.panel.destroying && !self.panel.destroyed) {
11326                        // only the inner is being observed so if a change occurs we can safely just call resize
11327                        self.resize();
11328                    }
11329                }, 50));
11330                self.robserver.observe(self.$inner.get(0));
11331                return true;
11332            }
11333            return false;
11334        },
11335        doDestroy: function(){
11336            if (this.robserver instanceof ResizeObserver){
11337                this.robserver.disconnect();
11338            }
11339            this.$inner.fgswipe("destroy");
11340            return this._super();
11341        },
11342        doTransition: function($element, trigger){
11343            var element = $element.get(0);
11344            if (!element || typeof element.getAnimations !== "function"){
11345                return _t.start($element, trigger, null, 350);
11346            }
11347            return $.Deferred(function(def){
11348                requestAnimationFrame(function(){
11349                    trigger($element);
11350                    var animations = element.getAnimations().filter(function(animation){
11351                        var timing = animation.effect && typeof animation.effect.getComputedTiming === "function"
11352                            ? animation.effect.getComputedTiming()
11353                            : null;
11354                        return animation.playState !== "paused"
11355                            && timing !== null
11356                            && typeof timing.endTime === "number"
11357                            && isFinite(timing.endTime);
11358                    });
11359                    if (animations.length === 0){
11360                        def.resolve();
11361                        return;
11362                    }
11363                    Promise.all(animations.map(function(animation){
11364                        return animation.finished;
11365                    })).then(def.resolve, def.reject);
11366                });
11367            }).promise();
11368        },
11369        doLoad: function(media, reverseTransition){
11370            var self = this, states = self.panel.cls.states;
11371            return $.Deferred(function (def) {
11372                if (!media.isCreated) media.create();
11373                media.$el.toggleClass(states.reverse, reverseTransition);
11374                media.appendTo(self.$inner);
11375                var wait = [];
11376                if (self.panel.hasTransition){
11377                    wait.push(self.doTransition(media.$el, function($el){
11378                        $el.addClass(states.visible);
11379                    }));
11380                } else {
11381                    media.$el.addClass(states.visible);
11382                }
11383                wait.push(media.load());
11384                $.when.apply($, wait).then(def.resolve).fail(def.reject);
11385            }).promise();
11386        },
11387        doUnload: function(media, reverseTransition){
11388            var self = this, states = self.panel.cls.states;
11389            return $.Deferred(function (def) {
11390                var wait = [];
11391                if (media.isCreated){
11392                    media.$el.toggleClass(states.reverse, !reverseTransition);
11393                    if (self.panel.hasTransition){
11394                        wait.push(self.doTransition(media.$el, function($el){
11395                            $el.removeClass(states.visible);
11396                        }));
11397                    } else {
11398                        media.$el.removeClass(states.visible);
11399                    }
11400                }
11401                wait.push(media.unload());
11402                $.when.apply($, wait).then(def.resolve).fail(def.reject);
11403            }).always(function(){
11404                if (media.isCreated){
11405                    media.$el.removeClass(states.reverse);
11406                }
11407                media.detach();
11408            }).promise();
11409        },
11410        onSwipe: function(info, data){
11411            var self = data.self;
11412            if (info.direction === "E"){
11413                self.panel.prev();
11414            }
11415            if (info.direction === "W"){
11416                self.panel.next();
11417            }
11418        },
11419        resize: function(){
11420            if (this.currentMedia instanceof _.Panel.Media){
11421                this.currentMedia.resize();
11422            }
11423        }
11424    });
11425
11426})(
11427    FooGallery.$,
11428    FooGallery,
11429    FooGallery.utils.fn,
11430    FooGallery.utils.transition
11431);
11432
11433(function($, _, _icons, _utils, _is, _fn, _obj){
11434
11435    /**
11436     * @memberof FooGallery.Panel.
11437     * @class Area
11438     * @augments FooGallery.Panel.SideArea
11439     */
11440    _.Panel.SideArea = _.Panel.Area.extend(/** @lends FooGallery.Panel.SideArea */{
11441        /**
11442         * @ignore
11443         * @constructs
11444         * @param panel
11445         * @param name
11446         * @param options
11447         * @param classes
11448         */
11449        construct: function(panel, name, options, classes){
11450            var self = this, cls = panel.cls.sideArea;
11451            self._super(panel, name, _obj.extend({
11452                icon: null,
11453                label: null,
11454                position: null,
11455                overlay: false,
11456                visible: true,
11457                autoHide: false,
11458                toggle: !!panel.opt.buttons[name],
11459                priority: 99
11460            }, options), _obj.extend({
11461                toggle: this.__cls(cls.toggle, name, true),
11462                visible: this.__cls(cls.visible, name),
11463                overlay: this.__cls(cls.overlay, name),
11464                position: {
11465                    top: this.__cls(cls.position.top, name),
11466                    right: this.__cls(cls.position.right, name),
11467                    bottom: this.__cls(cls.position.bottom, name),
11468                    left: this.__cls(cls.position.left, name),
11469                }
11470            }, classes));
11471            self.isVisible = self.opt.visible;
11472            self.allPositionClasses = Object.keys(self.cls.position).map(function (key) {
11473                return self.cls.position[key];
11474            }).join(" ") + " " + self.cls.overlay;
11475            self.button = self.registerButton();
11476        },
11477        registerButton: function(){
11478            var btn = new _.Panel.SideAreaButton(this);
11479            this.panel.buttons.register(btn, this.opt.priority);
11480            return btn;
11481        },
11482        doCreate: function(){
11483            if (this._super()){
11484                if (this.isEnabled()){
11485                    this.panel.$el.toggleClass(this.cls.visible, this.isVisible);
11486                    this.setPosition( this.opt.position );
11487                }
11488                return true;
11489            }
11490            return false;
11491        },
11492        isEnabled: function(){
11493            return this.cls.position.hasOwnProperty(this.opt.position);
11494        },
11495        canLoad: function(media){
11496            return this.isEnabled() && media instanceof _.Panel.Media;
11497        },
11498        getPosition: function(){
11499            if (this.isEnabled()){
11500                const position = this.cls.position[this.opt.position];
11501                return this.opt.overlay ? position + " " + this.cls.overlay : position;
11502            }
11503            return null;
11504        },
11505        /**
11506         *
11507         * @param {string} position
11508         * @param {?boolean} [overlay]
11509         */
11510        setPosition: function( position, overlay = null ){
11511            this.opt.position = this.cls.position.hasOwnProperty(position) ? position : null;
11512            if (_is.boolean(overlay)) this.opt.overlay = overlay;
11513            if (_is.jq(this.panel.$el)){
11514                this.panel.$el.removeClass(this.allPositionClasses).addClass(this.getPosition());
11515            }
11516        },
11517        toggle: function( visible ){
11518            this.isVisible = _is.boolean(visible) ? visible : !this.isVisible;
11519            if (_is.jq(this.panel.$el)) {
11520                this.panel.$el.toggleClass(this.cls.visible, this.isVisible);
11521            }
11522            this.panel.buttons.press( this.name, this.isVisible );
11523        },
11524        onToggleClick: function(e){
11525            e.preventDefault();
11526            e.data.self.toggle();
11527        },
11528        isTargetingSamePosition: function( area ) {
11529            if ( area instanceof _.Panel.SideArea ) {
11530                const pos1 = this.opt.position,
11531                    pos2 = area.opt.position;
11532                return _is.string( pos1 ) && _is.string( pos2 ) && pos1 === pos2;
11533            }
11534            return false;
11535        }
11536    });
11537
11538})(
11539    FooGallery.$,
11540    FooGallery,
11541    FooGallery.icons,
11542    FooGallery.utils,
11543    FooGallery.utils.is,
11544    FooGallery.utils.fn,
11545    FooGallery.utils.obj
11546);
11547(function($, _, _is, _fn){
11548
11549    /**
11550     * @memberof FooGallery.Panel.
11551     * @class Info
11552     * @augments FooGallery.Panel.SideArea
11553     */
11554    _.Panel.Info = _.Panel.SideArea.extend(/** @lends FooGallery.Panel.Info */{
11555        /**
11556         * @ignore
11557         * @constructs
11558         * @param panel
11559         */
11560        construct: function(panel){
11561            this._super(panel, "info", {
11562                icon: "info",
11563                label: panel.il8n.buttons.info,
11564                position: panel.opt.info,
11565                overlay: panel.opt.infoOverlay,
11566                visible: panel.opt.infoVisible,
11567                autoHide: panel.opt.infoAutoHide,
11568                align: panel.opt.infoAlign,
11569                waitForUnload: false,
11570                group: "overlay",
11571                priority: 90
11572            }, panel.cls.info);
11573        },
11574        doCreate: function(){
11575            var self = this;
11576            if (self.isEnabled() && self._super()) {
11577                if (_is.string(self.opt.align) && self.cls.align.hasOwnProperty(self.opt.align)){
11578                    self.panel.$el.addClass(self.cls.align[self.opt.align]);
11579                }
11580                return true;
11581            }
11582            return false;
11583        },
11584        canLoad: function(media){
11585            return this._super(media) && media.caption.canLoad();
11586        },
11587        doLoad: function(media, reverseTransition){
11588            if (this.canLoad(media)){
11589                media.caption.appendTo(this.$inner);
11590                media.caption.load();
11591            }
11592            return _fn.resolved;
11593        },
11594        doUnload: function(media, reverseTransition){
11595            media.caption.unload();
11596            media.caption.detach();
11597            return _fn.resolved;
11598        }
11599    });
11600
11601})(
11602    FooGallery.$,
11603    FooGallery,
11604    FooGallery.utils.is,
11605    FooGallery.utils.fn
11606);
11607(function($, _, _icons, _utils, _is, _fn, _t){
11608
11609    /**
11610     * @memberof FooGallery.Panel.
11611     * @class Thumbs
11612     * @augments FooGallery.Panel.SideArea
11613     */
11614    _.Panel.Thumbs = _.Panel.SideArea.extend(/** @lends FooGallery.Panel.Thumbs */{
11615        /**
11616         * @ignore
11617         * @constructs
11618         * @param panel
11619         */
11620        construct: function(panel){
11621            this._super(panel, "thumbs", {
11622                icon: "thumbs",
11623                label: panel.il8n.buttons.thumbs,
11624                position: panel.opt.thumbs,
11625                captions: panel.opt.thumbsCaptions,
11626                align: panel.opt.thumbsCaptionsAlign,
11627                small: panel.opt.thumbsSmall,
11628                bestFit: panel.opt.thumbsBestFit,
11629                toggle: false,
11630                waitForUnload: false
11631            }, panel.cls.thumbs);
11632
11633            this.iobserver = null;
11634            this.robserver = null;
11635            this.$prev = null;
11636            this.$next = null;
11637            this.$viewport = null;
11638            this.$stage = null;
11639            this.$dummy = null;
11640
11641            this.__items = [];
11642            this.__animationFrameId = null;
11643
11644            this.info = this.getInfo();
11645            this.selectedIndex = 0;
11646            this.scrollIndex = 0;
11647            this.lastIndex = 0;
11648        },
11649        isHorizontal: function(){
11650            return ["top","bottom"].indexOf(this.opt.position) !== -1;
11651        },
11652        doCreate: function(){
11653            var self = this;
11654            if (self.isEnabled() && self._super()){
11655                if (!self.opt.captions) self.panel.$el.addClass(self.cls.noCaptions);
11656                if (self.opt.small) self.panel.$el.addClass(self.cls.small);
11657                if (_is.string(self.opt.align) && self.cls.align.hasOwnProperty(self.opt.align)) self.panel.$el.addClass(self.cls.align[self.opt.align]);
11658
11659                self.$prev = $('<button/>', {type: 'button'}).addClass(self.cls.prev)
11660                    .append(_icons.get("arrow-left", self.panel.opt.icons))
11661                    .on("click.foogallery", {self: self}, self.onPrevClick)
11662                    .prependTo(self.$inner);
11663                self.$viewport = $('<div/>').addClass(self.cls.viewport).appendTo(self.$inner);
11664                self.$next = $('<button/>', {type: 'button'}).addClass(self.cls.next)
11665                    .append(_icons.get("arrow-right", self.panel.opt.icons))
11666                    .on("click.foogallery", {self: self}, self.onNextClick)
11667                    .appendTo(self.$inner);
11668                self.$stage = $('<div/>').addClass(self.cls.stage).appendTo(self.$viewport);
11669                self.$dummy = $('<div/>').addClass(self.cls.thumb.elem).appendTo(self.$viewport);
11670
11671                self.iobserver = new IntersectionObserver(function(entries){
11672                    entries.forEach(function(entry){
11673                        if (entry.isIntersecting){
11674                            self.iobserver.unobserve(entry.target);
11675                            self.loadThumbElement(entry.target);
11676                        }
11677                    });
11678                }, { root: self.$inner.get(0), rootMargin: "82px 300px" });
11679
11680                self.robserver = new ResizeObserver(_fn.throttle(function (entries) {
11681                    if (entries.length > 0 && self.panel instanceof _.Panel && !self.panel.destroying && !self.panel.destroyed) {
11682                        // only the viewport is being observed so if a change occurs we can safely grab just the first entry
11683                        var size = _utils.getResizeObserverSize(entries[0]), viewport = self.info.viewport;
11684                        var diffX = Math.floor(Math.abs(size.width - viewport.width)),
11685                            diffY = Math.floor(Math.abs(size.height - viewport.height));
11686                        if (self.isVisible && (diffX > 1 || diffY > 1)) {
11687                            self.resize();
11688                        }
11689                    }
11690                }, 50));
11691
11692                self.doCreateThumbs(self.panel.tmpl.items.available(self.panel.isVisible));
11693
11694                return true;
11695            }
11696            return false;
11697        },
11698        doCreateThumbs: function(items){
11699            if (_is.empty(items)) return;
11700            var self = this;
11701            if (self.iobserver instanceof IntersectionObserver){
11702                self.iobserver.takeRecords().forEach(function(entry){
11703                    self.iobserver.unobserve(entry.target);
11704                });
11705            }
11706            self.__items = items;
11707            self.selectedIndex = 0;
11708            self.scrollIndex = 0;
11709            self.lastIndex = self.__items.length - 1;
11710            self.$stage.empty();
11711            items.forEach(function(item){
11712                var $thumb = self.doCreateThumb(item).appendTo(self.$stage);
11713                self.iobserver.observe($thumb.get(0));
11714            });
11715            self.$stage.append($("<div/>").addClass(self.cls.spacer));
11716        },
11717        doCreateThumb: function(item){
11718            var self = this, cls = self.cls.thumb;
11719            return $("<figure/>").addClass(cls.elem).addClass(item.getTypeClass()).addClass(self.panel.cls.states.idle).append(
11720                $("<div/>").addClass(cls.media).append(
11721                    $("<div/>").addClass(cls.overlay),
11722                    $("<div/>").addClass(cls.wrap).append(
11723                        $("<img/>", {title: item.title, alt: item.alt}).attr({draggable: false}).addClass(cls.image)
11724                    ),
11725                    $("<div/>").addClass(self.panel.cls.loader)
11726                ),
11727                $("<div/>").addClass(cls.caption).append(
11728                    $("<div/>").addClass(cls.title).html(item.caption),
11729                    $("<div/>").addClass(cls.description).html(item.description)
11730                )
11731            ).data("item", item).on("click", {self: self, item: item}, self.onThumbClick);
11732        },
11733        doDestroy: function(){
11734            this.stopListening();
11735            if (this.iobserver instanceof IntersectionObserver){
11736                this.iobserver.disconnect();
11737            }
11738            if (this.robserver instanceof ResizeObserver){
11739                this.robserver.disconnect();
11740            }
11741            return this._super();
11742        },
11743        doLoad: function(media, reverseTransition){
11744            if (this.isCreated){
11745                var index = this.__items.indexOf(media.item);
11746                if (index !== -1) {
11747                    this.makeVisible(index);
11748                    this.$stage.find(this.sel.thumb.elem)
11749                        .removeClass(this.panel.cls.states.selected)
11750                        .eq(index).addClass(this.panel.cls.states.selected);
11751                    this.selectedIndex = index;
11752                }
11753            }
11754            return _fn.resolved;
11755        },
11756        makeVisible: function(index, disableTransition){
11757            if (index <= this.scrollIndex) {
11758                this.goto(index, disableTransition);
11759            } else if (index >= this.scrollIndex + this.info.count) {
11760                this.goto(index, disableTransition);
11761            }
11762        },
11763        listen: function(){
11764            var self = this;
11765            self.stopListening();
11766            if (self.isCreated){
11767                self.resize();
11768                self.robserver.observe(self.$viewport.get(0));
11769                self.$inner.fgswipe({data: {self: self}, swipe: self.onSwipe, allowPageScroll: true})
11770                    .on("DOMMouseScroll.foogallery-panel-thumbs mousewheel.foogallery-panel-thumbs", {self: self}, self.onMouseWheel);
11771            }
11772        },
11773        stopListening: function(){
11774            if (this.isCreated){
11775                this.$inner.fgswipe("destroy").off(".foogallery-panel-thumbs");
11776                this.$stage.find(this.sel.thumb).css({width: "", minWidth: "", height: "", minHeight: ""});
11777                this.robserver.unobserve(this.$viewport.get(0));
11778            }
11779        },
11780        loadThumbElement: function(element){
11781            var self = this,
11782                $thumb = $(element),
11783                item = $thumb.data("item"),
11784                img = $thumb.find(self.sel.thumb.image).get(0),
11785                states = self.panel.cls.states;
11786
11787            $thumb.removeClass(states.allLoading).addClass(states.loading);
11788            img.onload = function(){
11789                img.onload = img.onerror = null;
11790                $thumb.removeClass(states.allLoading).addClass(states.loaded);
11791            };
11792            img.onerror = function(){
11793                img.onload = img.onerror = null;
11794                $thumb.removeClass(states.allLoading).addClass(states.error);
11795            };
11796            img.src = item.src;
11797            img.srcset = item.srcset;
11798            if (img.complete){
11799                img.onload();
11800            }
11801        },
11802        goto: function(index, disableTransition){
11803            var self = this;
11804            if (!self.isCreated) return _fn.reject("thumbs not created");
11805
11806            index = index < 0 ? 0 : (index > self.lastIndex ? self.lastIndex : index);
11807
11808            var states = self.panel.cls.states,
11809                rightOrBottom = index >= self.scrollIndex + self.info.count, // position the thumb to the right or bottom of the viewport depending on orientation
11810                scrollIndex = rightOrBottom ? index - (self.info.count - 1) : index, // if rightOrBottom we subtract the count - 1 so the thumb appears to the right or bottom of the viewport
11811                maxIndex = self.lastIndex - (self.info.count - 1); // the scrollIndex of the last item
11812
11813            // fix any calculated value overflows
11814            if (scrollIndex < 0) scrollIndex = 0;
11815            if (maxIndex < 0) maxIndex = 0;
11816            if (scrollIndex > maxIndex) scrollIndex = maxIndex;
11817
11818            return $.Deferred(function(def){
11819                // find the thumb
11820                var $thumb = self.$stage.find(self.sel.thumb.elem).eq(scrollIndex);
11821                if ($thumb.length > 0){
11822                    // align the right or bottom edge of the thumb with the viewport
11823                    var alignRightOrBottom = scrollIndex > self.scrollIndex, hasFullStage = self.__items.length >= self.info.count, offset, translate;
11824                    if (self.info.isHorizontal) {
11825                        offset = -($thumb.prop("offsetLeft"));
11826                        if (alignRightOrBottom) offset += self.info.remaining.width;
11827                        if (hasFullStage && self.info.stage.width - Math.abs(offset) < self.info.viewport.width) {
11828                            offset = self.info.viewport.width - self.info.stage.width;
11829                        }
11830                        translate = "translateX(" + (offset - 1) + "px)";
11831                    } else {
11832                        offset = -($thumb.prop("offsetTop"));
11833                        if (alignRightOrBottom) offset += self.info.remaining.height;
11834                        if (hasFullStage && self.info.stage.height - Math.abs(offset) < self.info.viewport.height) {
11835                            offset = self.info.viewport.height - self.info.stage.height;
11836                        }
11837                        translate = "translateY(" + (offset - 1) + "px)";
11838                    }
11839                    if (self.panel.hasTransition && !disableTransition) {
11840                        _t.start(self.$stage, function ($el) {
11841                            $el.css("transform", translate);
11842                        }, null, 350).then(function () {
11843                            def.resolve();
11844                        }).fail(def.reject);
11845                    } else {
11846                        self.$stage.addClass(states.noTransitions).css("transform", translate);
11847                        self.$stage.prop("offsetHeight");
11848                        self.$stage.removeClass(states.noTransitions);
11849                        def.resolve();
11850                    }
11851                } else {
11852                    def.resolve();
11853                }
11854            }).always(function(){
11855                self.scrollIndex = scrollIndex;
11856                self.disableButton(self.$prev, scrollIndex <= 0, states.disabled);
11857                self.disableButton(self.$next, scrollIndex >= maxIndex, states.disabled);
11858            }).promise();
11859        },
11860        disableButton: function( $button, state, cls ) {
11861            $button.toggleClass( cls, state )
11862                .attr("aria-disabled", state)
11863                .prop("disabled", state);
11864        },
11865        getInfo: function(){
11866            var isHorizontal = this.isHorizontal(),
11867                viewport = { width: 0, height: 0 },
11868                stage = { width: 0, height: 0 },
11869                original = { width: 0, height: 0 },
11870                counts = { horizontal: 0, vertical: 0 },
11871                adjusted = { width: 0, height: 0 },
11872                remaining = { width: 0, height: 0 },
11873                width = 0, height = 0, itemCount = this.__items.length;
11874            if (this.isCreated){
11875                viewport = { width: this.$viewport.innerWidth() + 1, height: this.$viewport.innerHeight() + 1 };
11876                original = { width: this.$dummy.outerWidth(), height: this.$dummy.outerHeight() };
11877                counts = { horizontal: Math.floor(viewport.width / original.width), vertical: Math.floor(viewport.height / original.height) };
11878                adjusted = { width: viewport.width / Math.min(itemCount, counts.horizontal), height: viewport.height / Math.min(itemCount, counts.vertical) };
11879                width = this.opt.bestFit ? adjusted.width : original.width;
11880                height = this.opt.bestFit ? adjusted.height : original.height;
11881                stage = { width: isHorizontal ? this.__items.length * width : width, height: !isHorizontal ? this.__items.length * height : height };
11882                remaining = { width: Math.floor(viewport.width - (counts.horizontal * width)), height: Math.floor(viewport.height - (counts.vertical * height)) };
11883            }
11884            return {
11885                isHorizontal: isHorizontal,
11886                viewport: viewport,
11887                stage: stage,
11888                original: original,
11889                adjusted: adjusted,
11890                remaining: remaining,
11891                counts: counts,
11892                count: isHorizontal ? counts.horizontal : counts.vertical,
11893                width: width,
11894                height: height
11895            };
11896        },
11897        resize: function(){
11898            if (this.isCreated){
11899                this.info = this.getInfo();
11900                if (this.opt.bestFit){
11901                    if (this.info.isHorizontal){
11902                        this.$stage.find(this.sel.thumb.elem).css({width: this.info.width, minWidth: this.info.width, height: "", minHeight: ""});
11903                    } else {
11904                        this.$stage.find(this.sel.thumb.elem).css({height: this.info.height, minHeight: this.info.height, width: "", minWidth: ""});
11905                    }
11906                }
11907                this.goto(this.scrollIndex, true);
11908            }
11909        },
11910        onThumbClick: function(e){
11911            e.preventDefault();
11912            e.data.self.panel.load(e.data.item);
11913        },
11914        onPrevClick: function(e){
11915            e.preventDefault();
11916            var self = e.data.self;
11917            self.goto(self.scrollIndex - (self.info.count - 1 || 1));
11918        },
11919        onNextClick: function(e){
11920            e.preventDefault();
11921            var self = e.data.self;
11922            self.goto(self.scrollIndex + (self.info.count - 1 || 1));
11923        },
11924        onSwipe: function(info, data){
11925            var self = data.self, amount = 1;
11926            if (self.info.isHorizontal){
11927                amount = Math.ceil(info.distance / self.info.width);
11928                if (info.direction === "E"){
11929                    self.goto(self.scrollIndex - amount);
11930                }
11931                if (info.direction === "W"){
11932                    self.goto(self.scrollIndex + amount);
11933                }
11934            } else {
11935                amount = Math.ceil(info.distance / self.info.height);
11936                if (info.direction === "S"){
11937                    self.goto(self.scrollIndex - amount);
11938                }
11939                if (info.direction === "N"){
11940                    self.goto(self.scrollIndex + amount);
11941                }
11942            }
11943        },
11944        onMouseWheel: function(e){
11945            var self = e.data.self,
11946                delta = Math.max(-1, Math.min(1, (e.originalEvent.wheelDelta || -e.originalEvent.detail)));
11947            if (delta > 0){
11948                self.goto(self.scrollIndex - 1);
11949                e.preventDefault();
11950            } else if (delta < 0){
11951                self.goto(self.scrollIndex + 1);
11952                e.preventDefault();
11953            }
11954        }
11955    });
11956
11957})(
11958    FooGallery.$,
11959    FooGallery,
11960    FooGallery.icons,
11961    FooGallery.utils,
11962    FooGallery.utils.is,
11963    FooGallery.utils.fn,
11964    FooGallery.utils.transition
11965);
11966
11967(function($, _, _utils, _is, _fn, _obj, _str, _t){
11968
11969    _.Panel.Media = _utils.Class.extend({
11970        construct: function(panel, item){
11971            var self = this;
11972
11973            self.panel = panel;
11974
11975            self.item = item;
11976
11977            self.opt = _obj.extend({}, panel.opt.media);
11978
11979            self.cls = _obj.extend({}, panel.cls.media);
11980
11981            self.sel = _obj.extend({}, panel.sel.media);
11982
11983            self.il8n = _obj.extend({}, panel.il8n.media);
11984
11985            self.caption = new _.Panel.Media.Caption(panel, self);
11986
11987            if ( _.Panel.Media.Product ){
11988                self.product = new _.Panel.Media.Product(panel, self);
11989            }
11990
11991            if ( _.Panel.Media.Comments ){
11992                self.comments = new _.Panel.Media.Comments(panel, self);
11993            }
11994
11995            self.$el = null;
11996
11997            self.$content = null;
11998
11999            self.isCreated = false;
12000
12001            self.isAttached = false;
12002
12003            self.isLoading = false;
12004
12005            self.isLoaded = false;
12006
12007            self.isError = false;
12008        },
12009        getSize: function(attrWidth, attrHeight, defWidth, defHeight){
12010            var self = this, size = {};
12011            if (!_is.string(attrWidth) || !_is.string(attrHeight)) return size;
12012
12013            size[attrWidth] = _is.size(defWidth) ? defWidth : null;
12014            size[attrHeight] = _is.size(defHeight) ? defHeight : null;
12015
12016
12017            if (!self.item.isCreated) return size;
12018
12019            size[attrWidth] = self.item.$anchor.data(attrWidth) || size[attrWidth];
12020            size[attrHeight] = self.item.$anchor.data(attrHeight) || size[attrHeight];
12021            return size;
12022        },
12023        getSizes: function(){
12024            var self = this,
12025                size = self.getSize("width", "height", self.opt.width, self.opt.height),
12026                max = self.getSize("maxWidth", "maxHeight", self.opt.maxWidth, self.opt.maxHeight),
12027                min = self.getSize("minWidth", "minHeight", self.opt.minWidth, self.opt.minHeight);
12028            return _obj.extend(size, max, min);
12029        },
12030        destroy: function(){
12031            var self = this;
12032            var e = self.panel.trigger("media-destroy", [self]);
12033            if (!e.isDefaultPrevented()) {
12034                self.isCreated = !self.doDestroy();
12035            }
12036            if (!self.isCreated) {
12037                self.panel.trigger("media-destroyed", [self]);
12038            }
12039            return !self.isCreated;
12040        },
12041        doDestroy: function(){
12042            var self = this;
12043            if (self.isCreated){
12044                self.caption.destroy();
12045                self.detach();
12046                self.$el.remove();
12047            }
12048            return true;
12049        },
12050        create: function(){
12051            var self = this;
12052            if (!self.isCreated && _is.string(self.item.href)) {
12053                var e = self.panel.trigger("media-create", [self]);
12054                if (!e.isDefaultPrevented()) {
12055                    self.isCreated = self.doCreate();
12056                }
12057                if (self.isCreated) {
12058                    self.panel.trigger("media-created", [self]);
12059                }
12060            }
12061            return self.isCreated;
12062        },
12063        doCreate: function(){
12064            var self = this;
12065            self.$el = $('<div/>').addClass([self.cls.elem, self.cls.type].join(" ")).append(
12066                $('<div/>').addClass(self.panel.cls.loader)
12067            );
12068            self.$content = self.doCreateContent().addClass(self.cls.content).css(self.getSizes()).appendTo(self.$el);
12069            return true;
12070        },
12071        doCreateContent: function(){
12072            return $();
12073        },
12074        appendTo: function( parent ){
12075            var self = this;
12076            if (!self.isCreated){
12077                self.create();
12078            }
12079            if (self.isCreated && !self.isAttached){
12080                var e = self.panel.trigger("media-append", [self, parent]);
12081                if (!e.isDefaultPrevented()) {
12082                    self.isAttached = self.doAppendTo( parent );
12083                }
12084                if (self.isAttached) {
12085                    self.panel.trigger("media-appended", [self, parent]);
12086                }
12087            }
12088            return self.isAttached;
12089        },
12090        doAppendTo: function( parent ){
12091            this.$el.appendTo( parent );
12092            return this.$el.parent().length > 0;
12093        },
12094        detach: function(){
12095            var self = this;
12096            if (self.isCreated && self.isAttached) {
12097                var e = self.panel.trigger("media-detach", [self]);
12098                if (!e.isDefaultPrevented()) {
12099                    self.isAttached = !self.doDetach();
12100                }
12101                if (!self.isAttached) {
12102                    self.panel.trigger("media-detached", [self]);
12103                }
12104            }
12105            return !self.isAttached;
12106        },
12107        doDetach: function(){
12108            this.$el.detach();
12109            return true;
12110        },
12111        load: function(){
12112            var self = this, states = self.panel.cls.states;
12113            return $.Deferred(function(def){
12114                var e = self.panel.trigger("media-load", [self]);
12115                if (e.isDefaultPrevented()){
12116                    def.rejectWith("default prevented");
12117                    return;
12118                }
12119                if ( self.isLoaded ) {
12120                    def.resolve();
12121                } else if ( self.isError ) {
12122                    def.reject('previous error');
12123                } else {
12124                    self.isLoading = true;
12125                    self.$el.removeClass(states.allLoading).addClass(states.loading);
12126                    self.doLoad().then(def.resolve).fail(def.reject);
12127                }
12128            }).always(function(){
12129                self.isLoading = false;
12130                self.$el.removeClass(states.loading);
12131            }).then(function(){
12132                self.isLoaded = true;
12133                self.$el.addClass(states.loaded);
12134                self.panel.trigger("media-loaded", [self]);
12135            }).fail(function(){
12136                self.isError = true;
12137                self.$el.addClass(states.loaded);
12138                self.panel.trigger("media-error", [self]);
12139            }).promise();
12140        },
12141        doLoad: function(){
12142            return _fn.resolved;
12143        },
12144        unload: function(){
12145            var self = this;
12146            return $.Deferred(function(def){
12147                if (!self.isCreated || !self.isAttached){
12148                    def.rejectWith("not created or attached");
12149                    return;
12150                }
12151                var e = self.panel.trigger("media-unload", [self]);
12152                if (e.isDefaultPrevented()){
12153                    def.rejectWith("default prevented");
12154                    return;
12155                }
12156                self.doUnload().then(def.resolve).fail(def.reject);
12157            }).then(function(){
12158                self.panel.trigger("media-unloaded", [self]);
12159            }).promise();
12160        },
12161        doUnload: function(){
12162            return _fn.resolved;
12163        },
12164        resize: function(){}
12165    });
12166
12167    _.template.configure("core", {
12168        panel: {
12169            media: {
12170                width: null,
12171                height: null,
12172                minWidth: null,
12173                minHeight: null,
12174                maxWidth: null,
12175                maxHeight: null,
12176                attrs: {}
12177            }
12178        }
12179    },{
12180        panel: {
12181            media: {
12182                elem: "fg-media",
12183                type: "fg-media-unknown",
12184                content: "fg-media-content",
12185                caption: {
12186                    elem: "fg-media-caption",
12187                    title: "fg-media-caption-title",
12188                    description: "fg-media-caption-description",
12189                    exif: {
12190                        elem: "fg-media-caption-exif",
12191                        auto: "fg-media-caption-exif-auto",
12192                        full: "fg-media-caption-exif-full",
12193                        partial: "fg-media-caption-exif-partial",
12194                        minimal: "fg-media-caption-exif-minimal",
12195                        prop: "fg-media-caption-exif-prop",
12196                        icon: "fg-media-caption-exif-icon",
12197                        content: "fg-media-caption-exif-content",
12198                        label: "fg-media-caption-exif-label",
12199                        value: "fg-media-caption-exif-value",
12200                        tooltip: "fg-media-caption-exif-tooltip",
12201                        tooltipPointer: "fg-media-caption-exif-tooltip-pointer",
12202                        showTooltip: "fg-media-caption-exif-show-tooltip"
12203                    }
12204                },
12205                product: {
12206                    elem: "fg-media-product",
12207                    inner: "fg-media-product-inner",
12208                    header: "fg-media-product-header",
12209                    body: "fg-media-product-body",
12210                    footer: "fg-media-product-footer",
12211                    button: "fg-panel-button",
12212                    primary: "fg-panel-button-primary",
12213                    secondary: "fg-panel-button-secondary",
12214                    hidden: "fg-hidden",
12215                    disabled: "fg-disabled",
12216                    loading: "fg-loading"
12217                }
12218            }
12219        }
12220    }, {
12221        panel: {
12222            media: {
12223                product: {
12224                    title: "Product Information",
12225                    addToCart: "Add to Cart",
12226                    viewProduct: "View Product",
12227                    goToCheckout: "Go to Checkout",
12228                    success: "Successfully added to cart.",
12229                    error: "Something went wrong adding to cart."
12230                }
12231            }
12232        }
12233    });
12234
12235    _.Panel.media = new _.Factory();
12236
12237})(
12238    FooGallery.$,
12239    FooGallery,
12240    FooGallery.utils,
12241    FooGallery.utils.is,
12242    FooGallery.utils.fn,
12243    FooGallery.utils.obj,
12244    FooGallery.utils.str,
12245    FooGallery.utils.transition
12246);
12247
12248(function ($, _, _icons, _utils, _is, _fn, _obj, _str, _t) {
12249
12250    var canHover = !!window.matchMedia && window.matchMedia("(hover: hover)").matches;
12251
12252    _.Panel.Media.Caption = _utils.Class.extend({
12253        construct: function (panel, media) {
12254            var self = this;
12255            self.panel = panel;
12256            self.media = media;
12257            self.opt = panel.opt;
12258            self.cls = media.cls.caption;
12259            self.sel = media.sel.caption;
12260            self.$el = null;
12261            self.title = null;
12262            self.description = null;
12263            self.isCreated = false;
12264            self.isAttached = false;
12265            self.hasTitle = false;
12266            self.hasDescription = false;
12267            self.hasExif = false;
12268            self.init(media.item);
12269        },
12270        canLoad: function(){
12271            return this.hasTitle || this.hasDescription || this.hasExif;
12272        },
12273        init: function(item){
12274            if (!(item instanceof _.Item)) return;
12275            var self = this, title, desc, supplied = false;
12276            if (item.isCreated){
12277                var data = item.$anchor.data() || {};
12278                title = _is.string(data.lightboxTitle);
12279                desc = _is.string(data.lightboxDescription);
12280                if (title || desc){
12281                    supplied = true;
12282                    self.title = _.safeParse( title ? data.lightboxTitle : "" );
12283                    self.description = _.safeParse( desc ? data.lightboxDescription : "" );
12284                }
12285            } else {
12286                var attr = item.attr.anchor;
12287                title = _is.string(attr["data-lightbox-title"]);
12288                desc = _is.string(attr["data-lightbox-description"]);
12289                if (title || desc){
12290                    supplied = true;
12291                    self.title = _.safeParse( title ? attr["data-lightbox-title"] : "" );
12292                    self.description = _.safeParse( desc ? attr["data-lightbox-description"] : "" );
12293                }
12294            }
12295            if (!supplied){
12296                self.title = item.caption;
12297                self.description = item.description;
12298            }
12299            self.hasTitle = !_is.empty(self.title);
12300            self.hasDescription = !_is.empty(self.description);
12301            self.hasExif = item.hasExif && _utils.inArray(self.opt.exif, ["auto","full","partial","minimal"]) !== -1;
12302        },
12303        create: function(){
12304            if (!this.isCreated){
12305                var e = this.panel.trigger("caption-create", [this]);
12306                if (!e.isDefaultPrevented()){
12307                    this.isCreated = this.doCreate();
12308                    if (this.isCreated){
12309                        this.panel.trigger("caption-created", [this]);
12310                    }
12311                }
12312            }
12313            return this.isCreated;
12314        },
12315        doCreate: function(){
12316            var self = this;
12317            self.$el = $("<div/>").addClass(self.cls.elem);
12318            if (self.hasTitle){
12319                self.$el.append($("<div/>").addClass(self.cls.title).html(self.title));
12320            }
12321            if (self.hasDescription){
12322                self.$el.append($("<div/>").addClass(self.cls.description).html(self.description));
12323            }
12324            if (self.hasExif){
12325                var exif = self.media.item.exif, $exif = $("<div/>", {"class": self.cls.exif.elem}).addClass(self.cls.exif[self.opt.exif]);
12326                _.supportedExifProperties.forEach(function(prop){
12327                    if (!_is.empty(exif[prop])){
12328                        var icon = "exif-" + _str.kebab(prop), text = self.media.item.il8n.exif[prop], value = exif[prop];
12329                        var $exifProp = $("<div/>", {"class": self.cls.exif.prop}).append(
12330                            $("<div/>", {"class": self.cls.exif.icon}).append(_icons.get(icon, self.opt.icons)),
12331                            $("<div/>", {"class": self.cls.exif.content}).append(
12332                                $("<div/>", {"class": self.cls.exif.label}).text(text),
12333                                $("<div/>", {"class": self.cls.exif.value}).text(value)
12334                            ),
12335                            $("<span/>", {"class": self.cls.exif.tooltip}).text(text + ": " + value).append(
12336                                $("<span/>", {"class": self.cls.exif.tooltipPointer})
12337                            )
12338                        );
12339                        if (!canHover){
12340                            $exifProp.on("click", {self: self}
12340, self.onExifClick);
12341                        }
12342                        $exif.append($exifProp);
12343                    }
12344                });
12345                self.$el.append($exif);
12346            }
12347            return true;
12348        },
12349        onExifClick: function(e){
12350            e.preventDefault();
12351            var self = e.data.self, $this = $(this),
12352                $tooltip = $this.find(self.sel.exif.tooltip),
12353                $current = $(self.sel.exif.showTooltip);
12354
12355            $(self.sel.exif.prop).removeClass(self.cls.exif.showTooltip)
12356                .find(self.sel.exif.tooltip).css("left", "")
12357                .find(self.sel.exif.tooltipPointer).css("left", "");
12358            if (!$current.is($this)){
12359                $tooltip.css("display", "inline-block");
12360
12361                var left = $tooltip.offset().left,
12362                    right = left + $tooltip.outerWidth(),
12363                    diff = Math.ceil(right - window.innerWidth);
12364
12365                if (diff > 0){
12366                    $tooltip.css("left", "calc(50% - " + diff + "px)")
12367                        .find(self.sel.exif.tooltipPointer).css("left", "calc(50% + " + diff + "px)");
12368                }
12369                if (left < 0){
12370                    left = Math.abs(left);
12371                    $tooltip.css("left", "calc(50% + " + left + "px)")
12372                        .find(self.sel.exif.tooltipPointer).css("left", "calc(50% - " + left + "px)");
12373                }
12374
12375                $tooltip.css("display", "");
12376                $this.addClass(self.cls.exif.showTooltip);
12377            }
12378        },
12379        destroy: function(){
12380            if (this.isCreated){
12381                var e = this.panel.trigger("caption-destroy", [this]);
12382                if (!e.isDefaultPrevented()){
12383                    this.isCreated = !this.doDestroy();
12384                    if (!this.isCreated){
12385                        this.panel.trigger("caption-destroyed", [this]);
12386                    }
12387                }
12388            }
12389            return !this.isCreated;
12390        },
12391        doDestroy: function(){
12392            this.$el.remove();
12393            return true;
12394        },
12395        appendTo: function( parent ){
12396            var self = this;
12397            if (!self.isCreated){
12398                self.create();
12399            }
12400            if (self.isCreated && !self.isAttached){
12401                var e = self.panel.trigger("caption-append", [self, parent]);
12402                if (!e.isDefaultPrevented()) {
12403                    self.isAttached = self.doAppendTo( parent );
12404                }
12405                if (self.isAttached) {
12406                    self.panel.trigger("caption-appended", [self, parent]);
12407                }
12408            }
12409            return self.isAttached;
12410        },
12411        doAppendTo: function( parent ){
12412            this.$el.appendTo( parent );
12413            return this.$el.parent().length > 0;
12414        },
12415        detach: function(){
12416            var self = this;
12417            if (self.isCreated && self.isAttached) {
12418                var e = self.panel.trigger("caption-detach", [self]);
12419                if (!e.isDefaultPrevented()) {
12420                    self.isAttached = !self.doDetach();
12421                }
12422                if (!self.isAttached) {
12423                    self.panel.trigger("caption-detached", [self]);
12424                }
12425            }
12426            return !self.isAttached;
12427        },
12428        doDetach: function(){
12429            this.$el.detach();
12430            return true;
12431        },
12432        load: function(){
12433            var self = this, states = self.panel.cls.states;
12434            return $.Deferred(function(def){
12435                var e = self.panel.trigger("caption-load", [self]);
12436                if (e.isDefaultPrevented()){
12437                    def.rejectWith("default prevented");
12438                    return;
12439                }
12440                self.$el.removeClass(states.allLoading).addClass(states.loading);
12441                self.doLoad().then(def.resolve).fail(def.reject);
12442            }).always(function(){
12443                self.$el.removeClass(states.loading);
12444            }).then(function(){
12445                self.$el.addClass(states.loaded);
12446                self.panel.trigger("caption-loaded", [self]);
12447            }).fail(function(){
12448                self.$el.addClass(states.loaded);
12449                self.panel.trigger("caption-error", [self]);
12450            }).promise();
12451        },
12452        doLoad: function(){
12453            return _fn.resolved;
12454        },
12455        unload: function(){
12456            var self = this;
12457            return $.Deferred(function(def){
12458                if (!self.isCreated || !self.isAttached){
12459                    def.rejectWith("not created or attached");
12460                    return;
12461                }
12462                var e = self.panel.trigger("caption-unload", [self]);
12463                if (e.isDefaultPrevented()){
12464                    def.rejectWith("default prevented");
12465                    return;
12466                }
12467                self.doUnload().then(def.resolve).fail(def.reject);
12468            }).then(function(){
12469                self.panel.trigger("caption-unloaded", [self]);
12470            }).promise();
12471        },
12472        doUnload: function(){
12473            return _fn.resolved;
12474        }
12475    });
12476
12477})(
12478    FooGallery.$,
12479    FooGallery,
12480    FooGallery.icons,
12481    FooGallery.utils,
12482    FooGallery.utils.is,
12483    FooGallery.utils.fn,
12484    FooGallery.utils.obj,
12485    FooGallery.utils.str,
12486    FooGallery.utils.transition
12487);
12488
12489(function($, _, _utils, _is, _obj){
12490
12491    _.Panel.Image = _.Panel.Media.extend({
12492        construct: function(panel, item){
12493            this._super(panel, item);
12494            _obj.extend(this.opt, panel.opt.image);
12495            _obj.extend(this.cls, panel.cls.image);
12496            _obj.extend(this.sel, panel.sel.image);
12497            this.allFullClasses = [this.cls.fullWidth, this.cls.fullHeight].join(" ");
12498        },
12499        doCreateContent: function(){
12500            return $('<img/>').attr(this.opt.attrs);
12501        },
12502        resize: function(){
12503            var self = this;
12504            if (self.isCreated && self.panel.opt.fitMedia){
12505                var img = self.$content.get(0);
12506                if (img.naturalWidth && img.naturalHeight){
12507                    var landscape = img.naturalWidth >= img.naturalHeight,
12508                        fullWidth = landscape,
12509                        targetWidth = self.$el.innerWidth(),
12510                        targetHeight = self.$el.innerHeight(),
12511                        ratio;
12512
12513                    if (landscape){
12514                        ratio = targetWidth / img.naturalWidth;
12515                        if (img.naturalHeight * ratio < targetHeight){
12516                            fullWidth = false;
12517                        }
12518                    } else {
12519                        ratio = targetHeight / img.naturalHeight;
12520                        if (img.naturalWidth * ratio < targetWidth){
12521                            fullWidth = true;
12522                        }
12523                    }
12524                    _utils.requestFrame(function(){
12525                        self.$content.removeClass(self.allFullClasses).addClass(fullWidth ? self.cls.fullWidth : self.cls.fullHeight);
12526                    });
12527                }
12528            }
12529        },
12530        doLoad: function(){
12531            var self = this;
12532            return $.Deferred(function(def){
12533                var img = self.$content.get(0);
12534                img.alt = self.item.alt;
12535                img.onload = function () {
12536                    img.onload = img.onerror = null;
12537                    def.resolve(self);
12538                };
12539                img.onerror = function () {
12540                    img.onload = img.onerror = null;
12541                    def.rejectWith("error loading image");
12542                };
12543                const { opt: { cors } } = self.panel.tmpl;
12544                if ( _is.string( cors ) ) {
12545                    if ( img.crossOrigin === null && _.isCrossOrigin(self.item.href) ) {
12546                        img.crossOrigin = cors;
12547                    }
12548                }
12549                // set everything in motion by setting the src
12550                img.src = self.item.href;
12551                if (img.complete){
12552                    img.onload();
12553                }
12554            }).then(function(){
12555                self.resize();
12556            }).promise();
12557        }
12558    });
12559
12560    _.Panel.media.register("image", _.Panel.Image);
12561
12562    _.template.configure("core", {
12563        panel: {
12564            image: {
12565                attrs: {
12566                    draggable: false
12567                }
12568            }
12569        }
12570    },{
12571        panel: {
12572            image: {
12573                type: "fg-media-image",
12574                fullWidth: "fg-media-full-width",
12575                fullHeight: "fg-media-full-height"
12576            }
12577        }
12578    });
12579
12580})(
12581    FooGallery.$,
12582    FooGallery,
12583    FooGallery.utils,
12584    FooGallery.utils.is,
12585    FooGallery.utils.obj
12586);
12587(function($, _, _utils, _obj){
12588
12589    _.Panel.Iframe = _.Panel.Media.extend({
12590        construct: function(panel, item){
12591            this._super(panel, item);
12592            _obj.extend(this.opt, panel.opt.iframe);
12593            _obj.extend(this.cls, panel.cls.iframe);
12594            _obj.extend(this.sel, panel.sel.iframe);
12595        },
12596        doCreateContent: function(){
12597            return $('<iframe/>').attr(this.opt.attrs);
12598        },
12599        doLoad: function(){
12600            var self = this;
12601            return $.Deferred(function(def){
12602                self.$content.off("load error").on({
12603                    'load': function(){
12604                        self.$content.off("load error");
12605                        def.resolve(self);
12606                    },
12607                    'error': function(){
12608                        self.$content.off("load error");
12609                        def.reject(self);
12610                    }
12611                });
12612                self.$content.attr("src", self.item.href);
12613            }).promise();
12614        }
12615    });
12616
12617    _.Panel.media.register("iframe", _.Panel.Iframe);
12618
12619    _.template.configure("core", {
12620        panel: {
12621            iframe: {
12622                attrs: {
12623                    src: '',
12624                    frameborder: 'no',
12625                    allow: "autoplay; fullscreen",
12626                    allowfullscreen: true
12627                }
12628            }
12629        }
12630    },{
12631        panel: {
12632            iframe: {
12633                type: "fg-media-iframe"
12634            }
12635        }
12636    });
12637
12638})(
12639    FooGallery.$,
12640    FooGallery,
12641    FooGallery.utils,
12642    FooGallery.utils.obj
12643);
12644(function($, _, _utils, _obj, _str){
12645
12646    _.Panel.Html = _.Panel.Media.extend({
12647        construct: function(panel, item){
12648            this._super(panel, item);
12649            _obj.extend(this.opt, panel.opt.html);
12650            _obj.extend(this.cls, panel.cls.html);
12651            _obj.extend(this.sel, panel.sel.html);
12652            this.$target = null;
12653        },
12654        doCreate: function(){
12655            if (this._super()){
12656                if (!_str.startsWith(this.item.href, '#') || (this.$target = $(this.item.href)).length === 0){
12657                    this.$target = null;
12658                    return false;
12659                }
12660                return true;
12661            }
12662            return false;
12663        },
12664        doCreateContent: function(){
12665            return $('<div/>').attr(this.opt.attrs);
12666        },
12667        doAppendTo: function( parent ){
12668            if (this._super( parent )){
12669                this.$content.append(this.$target.contents());
12670                return true;
12671            }
12672            return false;
12673        },
12674        doDetach: function(){
12675            this.$target.append(this.$content.contents());
12676            return this._super();
12677        }
12678    });
12679
12680    _.Panel.media.register("html", _.Panel.Html);
12681
12682    _.template.configure("core", {
12683        panel: {
12684            html: {}
12685        }
12686    },{
12687        panel: {
12688            html: {
12689                type: "fg-media-html"
12690            }
12691        }
12692    });
12693
12694})(
12695    FooGallery.$,
12696    FooGallery,
12697    FooGallery.utils,
12698    FooGallery.utils.obj,
12699    FooGallery.utils.str
12700);
12701(function($, _, _is, _obj){
12702
12703    _.Lightbox = _.Panel.extend({
12704        construct: function (template, options) {
12705            var self = this;
12706            self._super(template, options);
12707            if (self.opt.enabled && (self.tmpl instanceof _.Template) && !(self.tmpl.destroying || self.tmpl.destroyed)) {
12708                self.tmpl.on({
12709                    "after-state": self.onAfterState,
12710                    "anchor-click-item": self.onAnchorClickItem,
12711                    "destroyed": self.onDestroyedTemplate
12712                }, self);
12713            }
12714        },
12715        onAnchorClickItem: function(e, item){
12716            if (!item.noLightbox){
12717                e.preventDefault();
12718                try {
12719                    this.open(item);
12720                } catch( err ) {
12721                    console.error( err );
12722                }
12723            }
12724        },
12725        onDestroyedTemplate: function(){
12726            this.destroy();
12727        },
12728        onAfterState: function(e, state){
12729            if (state.item instanceof _.Item && !state.item.noLightbox){
12730                try {
12731                    this.open(state.item);
12732                } catch( err ) {
12733                    console.error( err );
12734                }
12735            }
12736        }
12737    });
12738
12739    _.template.configure("core", {
12740        lightbox: {
12741            enabled: false
12742        }
12743    }, {});
12744
12745    _.Template.override("construct", function(options, element){
12746        this._super(options, element);
12747        var data = this.$el.data("foogalleryLightbox"),
12748            enabled = this.opt.lightbox.enabled || _is.hash(data) || (this.$el.length > 0 && this.el.hasAttribute("data-foogallery-lightbox"));
12749
12750        this.opt.lightbox = _obj.extend({}, this.opt.panel, this.opt.lightbox, { enabled: enabled }, data, this.opt.mobileLightbox);
12751        this.lightbox = enabled ? new _.Lightbox(this, this.opt.lightbox) : null;
12752    });
12753
12754})(
12755    FooGallery.$,
12756    FooGallery,
12757    FooGallery.utils.is,
12758    FooGallery.utils.obj
12759);
12760
12761(function($, _, _utils){
12762
12763	_.DefaultTemplate = _.Template.extend({});
12764
12765	_.template.register("default", _.DefaultTemplate, null, {
12766		container: "foogallery fg-default"
12767	});
12768
12769})(
12770	FooGallery.$,
12771	FooGallery,
12772	FooGallery.utils
12773);
12774(function($, _, _utils, _is){
12775
12776	/**
12777	 * @summary The Masonry template for FooGallery.
12778	 * @memberof FooGallery.
12779	 * @constructs MasonryTemplate
12780	 * @param {Object} [options] - The options for the template.
12781	 * @param {(jQuery|HTMLElement)} [element] - The jQuery object or HTMLElement of the template. If not supplied one will be created within the `parent` element supplied to the {@link FooGallery.Template#initialize|initialize} method.
12782	 * @augments FooGallery.Template
12783	 * @borrows FooGallery.utils.Class.extend as extend
12784	 * @borrows FooGallery.utils.Class.override as override
12785	 * @description This template makes use of the popular [Masonry library](http://masonry.desandro.com/) to perform its layout. It supports two basic layout types, fixed and column based.
12786	 */
12787	_.MasonryTemplate = _.Template.extend(/** @lends FooGallery.MasonryTemplate */{
12788		construct: function(options, element){
12789			var self = this;
12790			self._super(options, element);
12791			self.masonry = null;
12792			self.on({
12793				"pre-init": self.onPreInit,
12794				"destroyed": self.onDestroyed,
12795				"appended-items": self.onAppendedItems,
12796				"detach-item": self.onDetachItem,
12797				"layout after-filter-change": self.onLayoutRequired,
12798				"page-change": self.onPageChange
12799			}, self);
12800		},
12801		onPreInit: function(){
12802			var self = this, sel = self.sel,
12803				layout = self.template.layout,
12804				fixed;
12805
12806			if (["fixed", "col1", "col2", "col3", "col4", "col5", "col6"].indexOf(layout) !== -1){
12807				self.$el.removeClass("fg-fixed fg-col1 fg-col2 fg-col3 fg-col4 fg-col5 fg-col6").addClass("fg-" + layout);
12808			}
12809			fixed = self.$el.hasClass("fg-fixed");
12810
12811			self.template.isFitWidth = fixed;
12812			self.template.percentPosition = !fixed;
12813			self.template.transitionDuration = 0;
12814			self.template.itemSelector = sel.item.elem;
12815			if (!fixed){
12816				self.template.gutter = sel.gutterWidth;
12817				self.template.columnWidth = sel.columnWidth;
12818			}
12819			self.masonry = new Masonry( self.el, self.template );
12820		},
12821		onDestroyed: function(){
12822			var self = this;
12823			if (self.masonry instanceof Masonry){
12824				self.masonry.destroy();
12825			}
12826		},
12827		onLayoutRequired: function(){
12828			this.masonry.layout();
12829		},
12830		onPageChange: function(event, current, prev, isFilter){
12831			if (!isFilter){
12832				this.masonry.layout();
12833			}
12834		},
12835		onAppendedItems: function(event, items){
12836			var self = this,
12837				elements = items.map(function(item){
12838					return item.el;
12839				}),
12840				mItems = self.masonry.addItems(elements);
12841			// add and layout the new items with no transitions
12842			self.masonry.layoutItems(mItems, true);
12843		},
12844		onDetachItem: function(event, item){
12845			if (!event.isDefaultPrevented()){
12846				event.preventDefault();
12847				this.masonry.remove(item.el);
12848				item.$el.removeClass(this.cls.hidden);
12849				item.isAttached = false;
12850			}
12851		}
12852	});
12853
12854	_.template.register("masonry", _.MasonryTemplate, {
12855		template: {}
12856	}, {
12857		container: "foogallery fg-masonry",
12858		columnWidth: "fg-column-width",
12859		gutterWidth: "fg-gutter-width"
12860	});
12861
12862})(
12863		FooGallery.$,
12864		FooGallery,
12865		FooGallery.utils,
12866		FooGallery.utils.is
12867);
12868
12869(function($, _, _utils, _is){
12870
12871	_.Justified = _utils.Class.extend({
12872		construct: function(template, options){
12873			var self = this;
12874			self.tmpl = template;
12875			self.$el = template.$el;
12876			self.options = $.extend(true, {}, _.Justified.defaults, options);
12877			self._items = [];
12878			self.maxRowHeight = 0;
12879			self.borderSize = 0;
12880			self.align = ["left","center","right"].indexOf(self.options.align) !== -1 ? self.options.align : "center";
12881		},
12882		init: function(){
12883			var self = this;
12884			self.maxRowHeight = self.getMaxRowHeight(self.options.maxRowHeight, self.options.rowHeight);
12885			self.borderSize = self.getBorderSize();
12886		},
12887		destroy: function(){
12888			this.$el.removeAttr("style");
12889		},
12890		getBorderSize: function(){
12891			var border = this.tmpl.getCSSClass("border", "");
12892			switch (border){
12893				case "fg-border-thin":
12894					return 4;
12895				case "fg-border-medium":
12896					return 10;
12897				case "fg-border-thick":
12898					return 16;
12899				default:
12900					return 0;
12901			}
12902		},
12903		getMaxRowHeight: function(value, def) {
12904			if (_is.string(value)){
12905				var parsed = parseInt(value);
12906				if (isNaN(parsed)) return def;
12907				if (parsed <= 0) return Infinity;
12908				return value.indexOf('%') !== -1 ? def * (parsed / 100) : parsed;
12909			}
12910			if (_is.number(value)){
12911				if (value <= 0) return Infinity;
12912				return value;
12913			}
12914			return def;
12915		},
12916		layout: function(width){
12917			var self = this;
12918			if (!_is.number(width)){
12919				width = self.$el.width();
12920			}
12921			if (width > 0){
12922				var result = self.createRows(width);
12923				if (result.height !== 0 && result.rows.length > 0){
12924					self.$el.height(result.height);
12925					result.rows.forEach(function(row, i){
12926						self.render(row, i === result.rows.length - 1);
12927					});
12928				}
12929			}
12930		},
12931		render: function(row, isLast){
12932			var self = this, applyMaxHeight = !isLast && self.options.lastRow !== "justify";
12933			row.items.forEach(function(item){
12934				if (item.elem){
12935					if (row.visible){
12936						item.elem.style.setProperty("position", "absolute");
12937						item.elem.style.setProperty("width", item.width + "px");
12938						item.elem.style.setProperty("height", item.height + "px");
12939						item.elem.style.setProperty("top", item.top + "px");
12940						item.elem.style.setProperty("left", item.left + "px");
12941						item.elem.style.setProperty("margin", "0");
12942						item.elem.style.removeProperty("display");
12943						if (self.maxRowHeight > 0 && applyMaxHeight){
12944							item.elem.style.setProperty("max-height", (self.maxRowHeight + (self.borderSize * 2)) + "px");
12945						} else {
12946							item.elem.style.removeProperty("max-height");
12947						}
12948						if (!item.elem.classList.contains("fg-positioned")){
12949							item.elem.classList.add("fg-positioned");
12950						}
12951					} else {
12952						item.elem.style.setProperty("display", "none");
12953					}
12954				}
12955			});
12956		},
12957		justify: function(row, top, maxWidth, maxHeight){
12958			var self = this,
12959				margin = self.options.margins,
12960				margins = margin * (row.items.length - 1),
12961				max = maxWidth - margins,
12962				rowWidth = row.width - margins;
12963
12964			var w_ratio = max / rowWidth;
12965			row.width = rowWidth * w_ratio;
12966			row.height = row.height * w_ratio;
12967
12968			if (row.height > (maxHeight + (self.borderSize * 2))){
12969				var h_ratio = (maxHeight + (self.borderSize * 2)) / row.height;
12970				row.width = row.width * h_ratio;
12971				row.height = row.height * h_ratio;
12972			}
12973
12974			row.top = top;
12975			// default is left 0 because a full row starts at 0 and it matches default layouts
12976			row.left = 0;
12977			// if we don't have a full row and align !== left
12978			if (self.align !== "left" && row.width < max){
12979				if (self.align === "right"){
12980					row.left = max - row.width;
12981				} else {
12982					row.left = (max - row.width) / 2;
12983				}
12984			}
12985
12986			row.width += margins;
12987
12988			var left = row.left;
12989			row.items.forEach(function(item, i){
12990				if (i > 0) left += margin;
12991				item.left = left;
12992				item.top = top;
12993				var i_ratio = row.height / item.height;
12994				item.width = item.width * i_ratio;
12995				item.height = item.height * i_ratio;
12996				left += item.width;
12997			});
12998
12999			return row.height;
13000		},
13001		createRows: function(maxWidth){
13002			var self = this,
13003				margin = self.options.margins,
13004				items = self.tmpl.getAvailable(),
13005				rows = [],
13006				index = -1;
13007
13008			function newRow(){
13009				return {
13010					index: ++index,
13011					visible: true,
13012					width: 0,
13013					height: self.options.rowHeight + (self.borderSize * 2),
13014					top: 0,
13015					left: 0,
13016					items: []
13017				};
13018			}
13019
13020			function newItem(item, rowHeight){
13021				var width = item.width, height = item.height;
13022				// make the item match the row height
13023				if (height !== rowHeight){
13024					var ratio = rowHeight / height;
13025					height = height * ratio;
13026					width = width * ratio;
13027				}
13028				var maxRatio = self.maxRowHeight / rowHeight,
13029					maxWidth = width * maxRatio,
13030					maxHeight = height * maxRatio;
13031				return {
13032					__item: item,
13033					elem: item.el,
13034					width: width,
13035					height: height,
13036					maxWidth: maxWidth,
13037					maxHeight: maxHeight,
13038					top: 0,
13039					left: 0
13040				};
13041			}
13042
13043			var row = newRow(), top = 0, max = 0;
13044			items.forEach(function(fgItem){
13045				var item = newItem(fgItem, row.height);
13046				// adding this item to the row would exceed the max width
13047				if (row.width + item.width > maxWidth && row.items.length > 0){
13048					if (rows.length > 0) top += margin;
13049					var height = self.justify(row, top, maxWidth, self.maxRowHeight); // first justify the current row
13050					if (height > max) max = height;
13051					top += height;
13052					rows.push(row);
13053					row = newRow(); // then make the new one
13054				}
13055
13056				if (row.items.length > 0) row.width += margin;
13057				row.width += item.width;
13058				row.items.push(item);
13059			});
13060
13061			if (row.items.length > 0){
13062				var height, top_start = top + margin;
13063				switch (self.options.lastRow){
13064					case "smart":
13065						height = self.justify(row, top_start, maxWidth, self.maxRowHeight);
13066						if (max !== 0 && height > max){
13067							var h_ratio = max / height,
13068								w_ratio = (row.width * h_ratio) / maxWidth;
13069
13070							if (h_ratio < 0.9 || w_ratio < 0.9){
13071								height = self.justify(row, top_start, maxWidth, max - (self.borderSize * 2));
13072							}
13073						}
13074						break;
13075					case "justify":
13076						height = self.justify(row, top_start, maxWidth, 99999);
13077						break;
13078					case "hide":
13079						height = self.justify(row, top_start, maxWidth, self.maxRowHeight);
13080						if (row.width < maxWidth && rows.length > 1){
13081							row.visible = false;
13082						}
13083						break;
13084				}
13085				if (row.visible){
13086					top += height + margin;
13087				}
13088				rows.push(row);
13089			}
13090
13091			return {
13092				height: top,
13093				rows: rows
13094			};
13095		}
13096	});
13097
13098	_.Justified.defaults = {
13099		rowHeight: 150,
13100		maxRowHeight: "200%",
13101		margins: 0,
13102		align: "center",
13103		lastRow: "smart" // "smart","justify","hide"
13104	};
13105
13106})(
13107		FooGallery.$,
13108		FooGallery,
13109		FooGallery.utils,
13110		FooGallery.utils.is
13111);
13112(function($, _){
13113
13114	_.JustifiedTemplate = _.Template.extend({
13115		construct: function(options, element){
13116			var self = this;
13117			self._super(options, element);
13118			self.justified = null;
13119			self.on({
13120				"pre-init": self.onPreInit,
13121				"init": self.onInit,
13122				"destroyed": self.onDestroyed,
13123				"layout after-filter-change": self.onLayoutRequired,
13124				"page-change": self.onPageChange
13125			}, self);
13126		},
13127		onPreInit: function(){
13128			var self = this;
13129			self.justified = new _.Justified( self, self.template );
13130		},
13131		onInit: function(){
13132			this.justified.init();
13133		},
13134		onDestroyed: function(){
13135			var self = this;
13136			if (self.justified instanceof _.Justified){
13137				self.justified.destroy();
13138			}
13139		},
13140		onLayoutRequired: function(){
13141			this.justified.layout(this.lastWidth);
13142		},
13143		onPageChange: function(event, current, prev, isFilter){
13144			if (!isFilter){
13145				this.justified.layout(this.lastWidth);
13146			}
13147		}
13148	});
13149
13150	_.template.register("justified", _.JustifiedTemplate, null, {
13151		container: "foogallery fg-justified"
13152	});
13153
13154})(
13155	FooGallery.$,
13156	FooGallery
13157);
13158(function($, _, _utils, _is, _fn){
13159
13160	_.PortfolioTemplate = _.Template.extend({});
13161
13162	_.template.register("simple_portfolio", _.PortfolioTemplate, {}, {
13163		container: "foogallery fg-simple_portfolio"
13164	});
13165
13166})(
13167		FooGallery.$,
13168		FooGallery,
13169	FooGallery.utils,
13170	FooGallery.utils.is,
13171	FooGallery.utils.fn
13172);
13173(function ($, _, _utils, _obj) {
13174
13175	_.ImageViewerTemplate = _.Template.extend({
13176		construct: function (options, element) {
13177			var self = this,
13178				dotsPosition = options && options.template ? options.template.dotsPosition : null,
13179                overlay = element.hasClass('fg-overlay-controls');
13180			if (["fg-dots-none", "fg-dots-left", "fg-dots-center", "fg-dots-right"].indexOf(dotsPosition) !== -1){
13181				element.removeClass("fg-dots-none fg-dots-left fg-dots-center fg-dots-right").addClass(dotsPosition);
13182			}
13183            if ( overlay && _.paging?.contains('dots') ) {
13184                var reg = _.paging.registered['dots'];
13185                options = _obj.extend({
13186                    cls: {
13187                        paging: reg.cls
13188                    },
13189                    il8n: {
13190                        paging: reg.il8n
13191                    }
13192                }, options, {
13193                    paging: {
13194                        pushOrReplace: "replace",
13195                        type: "dots",
13196                        size: 1,
13197                        position: element.hasClass('fg-dots-none') ? "none" : "bottom",
13198                        scrollToTop: false
13199                    }
13200                })
13201            } else {
13202                options = _obj.extend({}, options, {
13203                    paging: {
13204                        pushOrReplace: "replace",
13205                        type: "default",
13206                        size: 1,
13207                        position: "none",
13208                        scrollToTop: false
13209                    }
13210                });
13211            }
13212			self._super(options, element);
13213			/**
13214			 * @summary The jQuery object containing the inner element that wraps all items.
13215			 * @memberof FooGallery.ImageViewerTemplate#
13216			 * @name $inner
13217			 * @type {jQuery}
13218			 */
13219			self.$inner = $();
13220			/**
13221			 * @summary The jQuery object that displays the current image count.
13222			 * @memberof FooGallery.ImageViewerTemplate#
13223			 * @name $current
13224			 * @type {jQuery}
13225			 */
13226			self.$current = $();
13227			/**
13228			 * @summary The jQuery object that displays the current image count.
13229			 * @memberof FooGallery.ImageViewerTemplate#
13230			 * @name $current
13231			 * @type {jQuery}
13232			 */
13233			self.$total = $();
13234			self._autoplayTimeout = null;
13235			self._autoplayPaused = false;
13236			/**
13237			 * @summary The jQuery object for the previous button.
13238			 * @memberof FooGallery.ImageViewerTemplate#
13239			 * @name $prev
13240			 * @type {jQuery}
13241			 */
13242			self.$prev = $();
13243			/**
13244			 * @summary The jQuery object for the next button.
13245			 * @memberof FooGallery.ImageViewerTemplate#
13246			 * @name $next
13247			 * @type {jQuery}
13248			 */
13249			self.$next = $();
13250			/**
13251			 * @summary The CSS classes for the Image Viewer template.
13252			 * @memberof FooGallery.ImageViewerTemplate#
13253			 * @name cls
13254			 * @type {FooGallery.ImageViewerTemplate~CSSClasses}
13255			 */
13256			/**
13257			 * @summary The CSS selectors for the Image Viewer template.
13258			 * @memberof FooGallery.ImageViewerTemplate#
13259			 * @name sel
13260			 * @type {FooGallery.ImageViewerTemplate~CSSSelectors}
13261			 */
13262			self.on({
13263				"loaded-item error-item": self.onFirstItemReady,
13264				"pre-init": self.onPreInit,
13265				"init": self.onInit,
13266				"ready": self.onReady,
13267				"destroy": self.onDestroy,
13268				"append-item": self.onAppendItem,
13269				"after-page-change": self.onAfterPageChange,
13270				"after-filter-change": self.onAfterFilterChange
13271			}, self);
13272		},
13273		createChildren: function(){
13274			var self = this;
13275			return $("<div/>", {"class": self.cls.inner}).append(
13276					$("<div/>", {"class": self.cls.innerContainer}),
13277					$("<div/>", {"class": self.cls.controls}).append(
13278							$("<button/>", {"class": self.cls.prev, type: 'button', title: self.il8n.prev})
13279									.append($("<span/>", {text: self.il8n.prev})),
13280							$("<label/>", {"class": self.cls.count, text: self.il8n.count})
13281									.prepend($("<span/>", {"class": self.cls.countCurrent, text: "0"}))
13282									.append($("<span/>", {"class": self.cls.countTotal, text: "0"})),
13283							$("<button/>", {"class": self.cls.next, type: 'button', title: self.il8n.next})
13284									.append($("<span/>", {text: self.il8n.next}))
13285					)
13286			);
13287		},
13288		destroyChildren: function(){
13289			var self = this;
13290			self.$el.find(self.sel.inner).remove();
13291		},
13292        onFirstItemReady: function(event) {
13293            this.$el.removeClass("fg-not-ready");
13294        },
13295
13296		onPreInit: function(event){
13297			var self = this;
13298            self.$el.addClass("fg-not-ready");
13299			self.$inner = self.$el.find(self.sel.innerContainer);
13300			self.$current = self.$el.find(self.sel.countCurrent);
13301			self.$total = self.$el.find(self.sel.countTotal);
13302			self.$prev = self.$el.find(self.sel.prev);
13303            if ( !self.$prev.attr('title') ) {
13304                self.$prev.attr('title', self.il8n.prev);
13305            }
13306			self.$next = self.$el.find(self.sel.next);
13307            if ( !self.$next.attr('title') ) {
13308                self.$next.attr('title', self.il8n.next);
13309            }
13310		},
13311		onInit: function (event) {
13312			var self = this;
13313			if (self.template.attachFooBox) {
13314				self.$el.on('foobox.previous', {self: self}, self.onFooBoxPrev)
13315						.on('foobox.next', {self: self}, self.onFooBoxNext);
13316			}
13317			self.$prev.on('click', {self: self}, self.onPrevClick);
13318			self.$next.on('click', {self: self}, self.onNextClick);
13319			self.$el.on('mouseenter.foogallery-autoplay', {self: self}, self.onAutoplayMouseEnter)
13320					.on('mouseleave.foogallery-autoplay', {self: self}, self.onAutoplayMouseLeave);
13321		},
13322		onReady: function (event) {
13323			this.update();
13324			this.startAutoplay();
13325		},
13326		/**
13327		 * @summary Destroy the plugin cleaning up any bound events.
13328		 * @memberof FooGallery.ImageViewerTemplate#
13329		 * @function onDestroy
13330		 */
13331		onDestroy: function (event) {
13332			var self = this;
13333			if (self.template.attachFooBox) {
13334				self.$el.off({
13335					'foobox.previous': self.onFooBoxPrev,
13336					'foobox.next': self.onFooBoxNext
13337				});
13338			}
13339			self.$prev.off('click', self.onPrevClick);
13340			self.$next.off('click', self.onNextClick);
13341			self.$el.off('.foogallery-autoplay');
13342			self.clearAutoplay();
13343		},
13344		onAppendItem: function (event, item) {
13345			event.preventDefault();
13346			this.$inner.append(item.$el);
13347			item.isAttached = true;
13348		},
13349		onAfterPageChange: function(event, current, prev, isFilter){
13350			if (!isFilter){
13351				this.update();
13352				this.startAutoplay();
13353			}
13354		},
13355		onAfterFilterChange: function(event){
13356			this.update();
13357			this.startAutoplay();
13358		},
13359
13360		update: function(){
13361			if (this.pages){
13362				this.$current.text(this.pages.current);
13363				this.$total.text(this.pages.total);
13364			}
13365		},
13366		getAutoplaySeconds: function(){
13367			var seconds = parseInt(this.template.autoplay, 10);
13368			return isNaN(seconds) ? 0 : seconds;
13369		},
13370		clearAutoplay: function(){
13371			if (this._autoplayTimeout !== null){
13372				clearTimeout(this._autoplayTimeout);
13373				this._autoplayTimeout = null;
13374			}
13375		},
13376		startAutoplay: function(){
13377			var self = this,
13378				seconds = self.getAutoplaySeconds();
13379
13380			self.clearAutoplay();
13381
13382			if (self._autoplayPaused || seconds <= 0 || !self.pages || self.pages.total <= 1){
13383				return;
13384			}
13385
13386			if (!self.template.loop && self.pages.current >= self.pages.total){
13387				return;
13388			}
13389
13390			self._autoplayTimeout = setTimeout(function(){
13391				self._autoplayTimeout = null;
13392				self.next();
13393			}, seconds * 1000);
13394		},
13395		onAutoplayMouseEnter: function(e){
13396			e.data.self._autoplayPaused = true;
13397			e.data.self.clearAutoplay();
13398		},
13399		onAutoplayMouseLeave: function(e){
13400			e.data.self._autoplayPaused = false;
13401			e.data.self.startAutoplay();
13402		},
13403		/**
13404		 * @summary Navigate to the previous item in the collection.
13405		 * @memberof FooGallery.ImageViewerTemplate#
13406		 * @function prev
13407		 * @description If there is a previous item in the collection calling this method will navigate to it displaying its' image and updating the current image count.
13408		 */
13409		prev: function () {
13410			if (this.pages){
13411				if (this.template.loop && this.pages.current === 1){
13412					this.pages.last();
13413				} else {
13414					this.pages.prev();
13415				}
13416				this.update();
13417			}
13418		},
13419		/**
13420		 * @summary Navigate to the next item in the collection.
13421		 * @memberof FooGallery.ImageViewerTemplate#
13422		 * @function next
13423		 * @description If there is a next item in the collection calling this method will navigate to it displaying its' image and updating the current image count.
13424		 */
13425		next: function () {
13426			if (this.pages){
13427				if (this.template.loop && this.pages.current === this.pages.total){
13428					this.pages.first();
13429				} else {
13430					this.pages.next();
13431				}
13432				this.update();
13433			}
13434		},
13435		/**
13436		 * @summary Handles the `"foobox.previous"` event allowing the plugin to remain in sync with what is displayed in the lightbox.
13437		 * @memberof FooGallery.ImageViewerTemplate#
13438		 * @function onFooBoxPrev
13439		 * @param {jQuery.Event} e - The jQuery.Event object for the event.
13440		 */
13441		onFooBoxPrev: function (e) {
13442			e.data.self.prev();
13443		},
13444		/**
13445		 * @summary Handles the `"foobox.next"` event allowing the plugin to remain in sync with what is displayed in the lightbox.
13446		 * @memberof FooGallery.ImageViewerTemplate#
13447		 * @function onFooBoxNext
13448		 * @param {jQuery.Event} e - The jQuery.Event object for the event.
13449		 */
13450		onFooBoxNext: function (e) {
13451			e.data.self.next();
13452		},
13453		/**
13454		 * @summary Handles the `"click"` event of the previous button.
13455		 * @memberof FooGallery.ImageViewerTemplate#
13456		 * @function onPrevClick
13457		 * @param {jQuery.Event} e - The jQuery.Event object for the event.
13458		 */
13459		onPrevClick: function (e) {
13460			e.preventDefault();
13461			e.stopPropagation();
13462			e.data.self.prev();
13463		},
13464		/**
13465		 * @summary Handles the `"click"` event of the next button.
13466		 * @memberof FooGallery.ImageViewerTemplate#
13467		 * @function onNextClick
13468		 * @param {jQuery.Event} e - The jQuery.Event object for the event.
13469		 */
13470		onNextClick: function (e) {
13471			e.preventDefault();
13472			e.stopPropagation();
13473			e.data.self.next();
13474		}
13475	});
13476
13477	_.template.register("image-viewer", _.ImageViewerTemplate, {
13478		template: {
13479			attachFooBox: false,
13480			loop: false,
13481			autoplay: 0
13482		}
13483	}, {
13484		container: "foogallery fg-image-viewer",
13485		inner: "fiv-inner",
13486		innerContainer: "fiv-inner-container",
13487		controls: "fiv-ctrls",
13488		prev: "fiv-prev",
13489		next: "fiv-next",
13490		count: "fiv-count",
13491		countCurrent: "fiv-count-current",
13492		countTotal: "fiv-count-total"
13493	}, {
13494		prev: "Prev",
13495		next: "Next",
13496		count: "of"
13497	});
13498
13499})(
13500		FooGallery.$,
13501		FooGallery,
13502		FooGallery.utils,
13503		FooGallery.utils.obj
13504);
13505
13506(function($, _, _obj){
13507
13508	_.ThumbnailTemplate = _.Template.extend({
13509		construct: function (options, element) {
13510			this._super(_obj.extend({}, options, {
13511				filtering: {
13512					type: "none"
13513				},
13514				paging: {
13515					pushOrReplace: "replace",
13516					theme: "fg-light",
13517					type: "default",
13518					size: $(element).hasClass((options.cls.stacked)) ? 3 : 1,
13519					position: "none",
13520					scrollToTop: false
13521				}
13522			}), element);
13523		}
13524	});
13525
13526	_.template.register("thumbnail", _.ThumbnailTemplate, {
13527        template: {}
13528    }, {
13529		container: "foogallery fg-thumbnail",
13530        stacked: "fg-stacked"
13531	});
13532
13533})(
13534		FooGallery.$,
13535		FooGallery,
13536		FooGallery.utils.obj
13537);
13538(function($, _, _utils, _is, _obj, _t){
13539
13540    _.StackAlbum = _utils.Class.extend({
13541        construct: function(element, options){
13542            var self = this;
13543            self.$el = _is.jq(element) ? element : $(element);
13544            self.el = self.$el.get(0);
13545            self.opt = _obj.extend({}, _.StackAlbum.defaults, options, self.$el.data('foogallery'));
13546            self.$back = self.$el.find('.fg-header-back');
13547            self.$active = self.$el.find('.fg-header-active');
13548            self.$piles = self.$el.find('.fg-piles');
13549            self.piles = self.$piles.find('.fg-pile').map(function(i, el){
13550                return new _.StackAlbum.Pile(self, el, { index: i });
13551            }).get();
13552            self.ignoreResize = false;
13553            self.info = self.getLayoutInfo();
13554            self.robserver = new ResizeObserver(function (e) {
13555                if (!self.ignoreResize && self.$el.is(":visible")){
13556                    const width = self.$el.width();
13557                    if ( self.info.maxWidth !== width ) {
13558                        self.info = self.getLayoutInfo();
13559                        self.layout(true);
13560                    }
13561                }
13562            });
13563        },
13564        init: function(){
13565            var self = this;
13566            self.info = self.getLayoutInfo();
13567            self.piles.forEach(function(pile){
13568                pile.init();
13569            });
13570            self.$back.on('click.foogallery', {self: self}, self.onBackClick);
13571            self.layout(true);
13572            self.robserver.observe(self.el);
13573        },
13574        destroy: function(){
13575            var self = this;
13576            self.robserver.disconnect();
13577            self.$back.off('.foogallery');
13578            self.piles.forEach(function(pile){
13579                pile.destroy();
13580            });
13581        },
13582        // getLayoutInfo: function(){
13583        //     var self = this,
13584        //         space = self.opt.gutter + (self.opt.border*2);
13585        //     return {
13586        //         maxWidth: self.$el.width(),
13587        //         space: space,
13588        //         halfSpace: space/2,
13589        //         itemWidth: self.opt.itemWidth,
13590        //         itemHeight: self.opt.itemHeight,
13591        //         itemOuterWidth: self.opt.itemWidth + (self.opt.border*2),
13592        //         itemOuterHeight: self.opt.itemHeight + (self.opt.border*2),
13593        //         blockWidth: self.opt.itemWidth + space,
13594        //         blockHeight: self.opt.itemHeight + space,
13595        //         border: self.opt.border,
13596        //         doubleBorder: self.opt.border*2,
13597        //         gutter: self.opt.gutter,
13598        //         halfGutter: self.opt.gutter/2
13599        //     };
13600        // },
13601        getLayoutInfo: function(){
13602            const self = this,
13603                maxWidth = self.$el.width(),
13604                doubleBorder = self.opt.border * 2,
13605                space = self.opt.gutter + doubleBorder,
13606                maxWidthInner = maxWidth - space;
13607
13608            let ratio = 1;
13609            if ( self.opt.itemWidth > maxWidthInner ) {
13610                ratio = maxWidthInner / self.opt.itemWidth;
13611            }
13612
13613            const itemWidth = self.opt.itemWidth * ratio,
13614                itemHeight = self.opt.itemHeight * ratio;
13615
13616            return {
13617                maxWidth: maxWidth,
13618                space: space,
13619                halfSpace: space/2,
13620                itemWidth: itemWidth,
13621                itemHeight: itemHeight,
13622                itemOuterWidth: itemWidth + doubleBorder,
13623                itemOuterHeight: itemHeight + doubleBorder,
13624                blockWidth: itemWidth + space,
13625                blockHeight: itemHeight + space,
13626                border: self.opt.border,
13627                doubleBorder: doubleBorder,
13628                gutter: self.opt.gutter,
13629                halfGutter: self.opt.gutter/2
13630            };
13631        },
13632        layout: function(immediate){
13633            var self = this, size;
13634            if (immediate){
13635                self.$el.addClass('fg-disable-transitions');
13636                self.$el.prop('offsetWidth');
13637            }
13638            if (self.hasActive){
13639                size = self.activePile.layout();
13640                self.activePile.setPosition(0, 0, size.width, size.height);
13641                self.$piles.css({width: size.width + 'px', height: size.height + 'px'});
13642            } else {
13643                size = self.layoutPiles();
13644                self.$piles.css({width: size.width + 'px', height: size.height + 'px'});
13645            }
13646            if (immediate){
13647                setTimeout(function(){
13648                    self.$el.removeClass('fg-disable-transitions');
13649                }, 0);
13650            }
13651        },
13652        layoutPiles: function(callback){
13653            var self = this,
13654                rowWidth = 0, rowCount = 1, width = 0;
13655
13656            callback = _is.fn(callback) ? callback : function(){};
13657
13658            self.piles.forEach(function(pile){
13659                var left = rowWidth;
13660                rowWidth += self.info.blockWidth;
13661                if (rowWidth > self.info.maxWidth && left > 0){
13662                    left = 0;
13663                    rowWidth = self.info.blockWidth;
13664                    rowCount++;
13665                }
13666                var top = self.info.blockHeight * (rowCount - 1);
13667                callback(pile, self.info);
13668                pile.layoutCollapsed();
13669                pile.setPosition(top, left, self.info.blockWidth, self.info.blockHeight);
13670                // keep track of the max calculated width
13671                if (rowWidth > width) width = rowWidth;
13672            });
13673            return {
13674                width: width,
13675                height: self.info.blockHeight * rowCount
13676            };
13677        },
13678        setActive: function(pile){
13679            var self = this,
13680                previous = self.activePile,
13681                hadActive = previous instanceof _.StackAlbum.Pile,
13682                size;
13683
13684            pile = pile instanceof _.StackAlbum.Pile ? pile : null;
13685
13686            self.activePile = pile;
13687            self.hasActive = pile !== null;
13688
13689            if (hadActive){
13690                previous.collapse();
13691            }
13692
13693            self.ignoreResize = true;
13694            if (self.hasActive){
13695                self.piles.forEach(function(p){
13696                    if (p === pile) return;
13697                    p.hide(self.activePile);
13698                });
13699                size = self.activePile.expand();
13700                self.$active.text(pile.title);
13701                self.$el.addClass('fg-has-active');
13702            } else {
13703                size = self.layoutPiles(function(p){
13704                    p.show();
13705                });
13706                self.$el.removeClass('fg-has-active');
13707            }
13708            _t.start(self.$piles, function($el){
13709                $el.css({width: size.width + 'px', height: size.height + 'px'});
13710            }, null, 350).then(function(){
13711                self.ignoreResize = false;
13712            });
13713        },
13714        onBackClick: function(e){
13715            e.preventDefault();
13716            e.stopPropagation();
13717            e.data.self.setActive(null);
13718        }
13719    });
13720
13721    _.StackAlbum.defaults = {
13722        gutter: 50,
13723        itemWidth: 150,
13724        itemHeight: 150,
13725        border: 10,
13726        angleStep: 1,
13727        randomAngle: false
13728    };
13729
13730})(
13731    FooGallery.$,
13732    FooGallery,
13733    FooGallery.utils,
13734    FooGallery.utils.is,
13735    FooGallery.utils.obj,
13736    FooGallery.utils.transition
13737);
13738(function($, _, _utils, _is, _obj){
13739
13740    _.StackAlbum.Pile = _utils.Class.extend({
13741        construct: function(album, element, options){
13742            var self = this;
13743            self.album = album;
13744            self.$el = _is.jq(element) ? element : $(element);
13745            self.opt = _obj.extend({}, _.StackAlbum.Pile.defaults, options, self.$el.data());
13746            self.title = self.opt.title;
13747            self.description = self.opt.description;
13748            self.captionAlignment = ['left', 'center', 'right'].indexOf(self.opt.captionAlignment) !== -1 ? self.opt.captionAlignment : 'center';
13749            self.items = self.$el.find('.fg-pile-item').map(function(i, el){
13750                return new _.StackAlbum.Item(self, el, { index: i });
13751            }).get();
13752            self.$cover = self.$el.find('.fg-pile-cover').first();
13753            self.top = 0;
13754            self.left = 0;
13755            self.isExpanded = false;
13756        },
13757        init: function(){
13758            var self = this,
13759                opt = self.album.opt,
13760                availableAngles = self.getAngles(opt.angleStep),
13761                currentAngle = opt.randomAngle ? self.randomAngle(availableAngles) : opt.angleStep;
13762
13763            if ( self.$cover.length === 0 && self.items.length > 0 ) {
13764                const $coverContent = $('<div/>', {'class': 'fg-pile-cover-content fg-pile-caption-align-' + self.captionAlignment}).append(
13765                    $('<span/>', {'class': 'fg-pile-cover-title', text: self.opt.title}),
13766                    $('<span/>', {'class': 'fg-pile-cover-count', text: self.items.length})
13767                );
13768
13769                if ( self.description ) {
13770                    $coverContent.append(
13771                        $('<span/>', {'class': 'fg-pile-cover-description', html: self.description})
13772                    );
13773                }
13774
13775                self.$cover = $('<div/>', {'class': 'fg-pile-cover'}).append(
13776                    $coverContent
13777                );
13778                self.items[0].$el.addClass('fg-has-cover').append(self.$cover);
13779            }
13780            self.$cover.on('click.foogallery', {self: self}, self.onCoverClick);
13781            self.items.forEach(function(item, i){
13782                item.init();
13783                if (i > 3) return; // we only care about the first 4 items after init
13784                if (i === 0){
13785                    item.load();
13786                } else {
13787                    if (i % 2 === 0){
13788                        item.setAngle(-currentAngle);
13789                    } else {
13790                        item.setAngle(currentAngle);
13791                    }
13792                    if (opt.randomAngle){
13793                        currentAngle = self.randomAngle(availableAngles);
13794                    } else {
13795                        currentAngle += opt.angleStep;
13796                    }
13797                }
13798            });
13799        },
13800        destroy: function(){
13801            var self = this;
13802            self.$cover.remove();
13803            self.items.forEach(function(item, i){
13804                if (i === 0) item.$el.removeClass('fg-has-cover');
13805                item.destroy();
13806            });
13807        },
13808        getAngles: function(step){
13809            var result = [], i = 1;
13810            for (; i <= 3; i++){
13811                result.push(i * step);
13812            }
13813            return result;
13814        },
13815        randomAngle: function(available){
13816            var min = 0, max = available.length,
13817                index = Math.floor(Math.random() * (max - min) + min),
13818                angle = available.splice(index, 1);
13819            return angle.length === 1 ? angle[0] : 0;
13820        },
13821        setPosition: function(top, left, itemWidth, itemHeight){
13822            var self = this;
13823            self.top = top;
13824            self.left = left;
13825            if (_is.number(itemWidth) && _is.number(itemHeight)){
13826                self.$el.css({top: top + 'px', left: left + 'px', width: itemWidth + 'px', height: itemHeight + 'px'});
13827            } else {
13828                self.$el.css({top: top + 'px', left: left + 'px'});
13829            }
13830        },
13831        layout: function(){
13832            const self = this,
13833                info = self.album.info;
13834
13835            if ( !self.isExpanded ) {
13836                return self.layoutCollapsed();
13837            }
13838
13839            let rowWidth = 0, rowCount = 1,
13840                isNew = false, width = 0;
13841
13842            self.items.forEach(function(item){
13843                rowWidth += info.halfGutter;
13844                if (rowWidth > info.maxWidth){
13845                    rowWidth = info.halfGutter;
13846                    rowCount++;
13847                    isNew = true;
13848                    console.log("A");
13849                }
13850                var left = rowWidth;
13851                rowWidth += info.itemOuterWidth + info.halfGutter;
13852                if (!isNew && rowWidth > info.maxWidth){
13853                    left = info.halfGutter;
13854                    rowWidth = info.blockWidth;
13855                    rowCount++;
13856                    console.log("B");
13857                }
13858                var top = (info.blockHeight * (rowCount - 1)) + info.halfGutter;
13859                isNew = false;
13860                item.setPosition(top, left, info.itemOuterWidth, info.itemOuterHeight);
13861                if (!item.isLoaded) item.load();
13862                // keep track of the max calculated width
13863                if (rowWidth > width) width = rowWidth;
13864            });
13865            return {
13866                width: width,
13867                height: info.blockHeight * rowCount
13868            };
13869        },
13870
13871        layoutCollapsed: function(){
13872            const self = this,
13873                info = self.album.info;
13874            self.items.forEach(function(item){
13875                item.setPosition(info.halfGutter, info.halfGutter, info.itemOuterWidth, info.itemOuterHeight);
13876            });
13877            return {
13878                width: info.blockWidth,
13879                height: info.blockHeight
13880            };
13881        },
13882
13883        expand: function(){
13884            var self = this, size;
13885            self.$el.removeClass('fg-collapsed').addClass('fg-expanded');
13886            self.isExpanded = true;
13887            size = self.layout();
13888            self.setPosition(0, 0, size.width, size.height);
13889            return size;
13890        },
13891        collapse: function(){
13892            var self = this, size;
13893            self.$el.removeClass('fg-expanded').addClass('fg-collapsed');
13894            self.isExpanded = false;
13895            size = self.layout();
13896            self.setPosition(0, 0, size.width, size.height);
13897            return size;
13898        },
13899        show: function(){
13900            var self = this;
13901            self.$el.removeClass('fg-hidden fg-expanded fg-collapsed');
13902        },
13903        hide: function(behind){
13904            var self = this;
13905            if (behind instanceof _.StackAlbum.Pile){
13906                self.setPosition(behind.top, behind.left);
13907            }
13908            self.$el.addClass('fg-hidden');
13909        },
13910
13911        onCoverClick: function(e){
13912            if ($(e.target).closest('a, button, input, select, textarea, [role="button"]').length !== 0){
13913                return;
13914            }
13915            e.preventDefault();
13916            e.stopPropagation();
13917            var self = e.data.self;
13918            self.album.setActive(self);
13919        }
13920    });
13921
13922    _.StackAlbum.Pile.defaults = {
13923        index: -1,
13924        title: null,
13925        captionAlignment: 'center',
13926        description: ''
13927    };
13928
13929})(
13930    FooGallery.$,
13931    FooGallery,
13932    FooGallery.utils,
13933    FooGallery.utils.is,
13934    FooGallery.utils.obj
13935);
13936
13937(function($, _, _utils, _is, _obj){
13938
13939    _.StackAlbum.Item = _utils.Class.extend({
13940        construct: function(pile, element, options){
13941            var self = this;
13942            self.pile = pile;
13943            self.$el = _is.jq(element) ? element : $(element);
13944            self.opt = _obj.extend({}, _.StackAlbum.Item.defaults, options, self.$el.data());
13945            self.$thumb = self.$el.find('.fg-pile-item-thumb');
13946            self.$image = self.$el.find('.fg-pile-item-image');
13947            self.isLoaded = false;
13948            self.isLoading = false;
13949            self._loading = null;
13950        },
13951        init: function(){
13952            const self = this,
13953                info = self.pile.album.info;
13954            self.$el.css({width: info.itemOuterWidth + 'px', height: info.itemOuterHeight + 'px'});
13955        },
13956        destroy: function(){
13957            const self = this;
13958            self.$el.css({top: '', left: '', width: '', height: '', transform: ''});
13959        },
13960        setAngle: function(angle){
13961            var self = this;
13962            self.$el.css({transform: 'rotate(' + angle + 'deg)'});
13963        },
13964        setPosition: function(top, left, itemWidth, itemHeight){
13965            var self = this;
13966            self.$el.css({top: top + 'px', left: left + 'px', width: itemWidth + 'px', height: itemHeight + 'px'});
13967        },
13968        load: function(){
13969            var self = this;
13970            if (_is.promise(self._loading)) return self._loading;
13971            return self._loading = $.Deferred(function(def){
13972                self.$el.addClass('fg-loading');
13973                self.isLoading = true;
13974                self.$image.on({
13975                    'load.foogallery': function(){
13976                        self.$image.off('.foogallery');
13977                        self.$el.removeClass('fg-loading');
13978                        self.isLoading = false;
13979                        self.isLoaded = true;
13980                        def.resolve();
13981                    },
13982                    'error.foogallery': function(){
13983                        self.$image.off('.foogallery');
13984                        self.$el.removeClass('fg-loading');
13985                        self.isLoading = false;
13986                        self.isLoaded = true;
13987                        def.reject();
13988                    }
13989                });
13990                self.$image.prop('src', self.$image.attr(self.opt.src))
13991                    .prop('srcset', self.$image.attr(self.opt.srcset));
13992            }).promise();
13993        }
13994    });
13995
13996    _.StackAlbum.Item.defaults = {
13997        index: -1,
13998        src: 'data-src-fg',
13999        srcset: 'data-srcset-fg'
14000    };
14001
14002})(
14003    FooGallery.$,
14004    FooGallery,
14005    FooGallery.utils,
14006    FooGallery.utils.is,
14007    FooGallery.utils.obj
14008);
14009(function ($, _, _utils) {
14010
14011    $.fn.foogalleryStackAlbum = function(options){
14012        return this.each(function(i, el){
14013            var $el = $(el), inst = $el.data('__FooGalleryAlbum__');
14014            if (inst instanceof _.StackAlbum) inst.destroy();
14015            inst = new _.StackAlbum($el);
14016            inst.init();
14017            $el.data('__FooGalleryAlbum__', inst);
14018        });
14019    };
14020
14021    _.loadStackAlbums = _.reloadStackAlbums = function(){
14022        // this automatically initializes all templates on page load
14023        $(function () {
14024            $('.foogallery-stack-album:not(.fg-ready)').foogalleryStackAlbum();
14025        });
14026
14027        _utils.ready(function () {
14028            $('.foogallery-stack-album.fg-ready').foogalleryStackAlbum();
14029        });
14030    };
14031
14032    _.loadStackAlbums();
14033
14034})(
14035    FooGallery.$,
14036    FooGallery,
14037    FooGallery.utils
14038);
14039(function($, _, _icons, _utils, _is){
14040
14041    _utils.Progress = _utils.EventClass.extend( {
14042        construct: function( tickRate ){
14043            const self = this;
14044            self._super();
14045            self.percent = 0;
14046            self.tickRate = _is.number( tickRate ) ? tickRate : 100;
14047            self.isPaused = false;
14048            self.isActive = false;
14049            self._intervalId = null;
14050            self._total = 0;
14051            self._target = null;
14052            self.onTick = self.onTick.bind( self );
14053        },
14054        destroy: function(){
14055            const self = this;
14056            self._reset();
14057            self._super();
14058        },
14059        _reset: function(){
14060            const self = this;
14061            if ( self._intervalId !== null ) clearInterval( self._intervalId );
14062            self.percent = 0;
14063            self._total = 0;
14064            self._intervalId = null;
14065            self._target = null;
14066            self.isActive = false;
14067            self.isPaused = false;
14068        },
14069        stop: function(){
14070            const self = this;
14071            if ( self.isActive ){
14072                self._reset();
14073                self.trigger( "stop" );
14074            }
14075        },
14076        start: function( seconds ){
14077            const self = this;
14078            self.stop();
14079            if ( !self.isActive && _is.number( seconds ) && seconds > 0 ){
14080                self._total = seconds * 1000;
14081                self._target = Date.now() + self._total;
14082                self._intervalId = setInterval( self.onTick, self.tickRate );
14083                self.isActive = true;
14084                self.trigger( "start" );
14085            }
14086        },
14087        pause: function(){
14088            const self = this;
14089            if ( self.isActive && !self.isPaused && self.percent < 100 ){
14090                if ( self._intervalId !== null ) clearInterval( self._intervalId );
14091                self._intervalId = null;
14092                self._target = null;
14093                self.isPaused = true;
14094                self.trigger( "pause" );
14095            }
14096        },
14097        resume: function(){
14098            const self = this;
14099            if ( self.isActive && self.isPaused ){
14100                const remaining = self._total - ( ( self._total * self.percent ) / 100 );
14101                self._target = Date.now() + remaining;
14102                self._intervalId = setInterval( self.onTick, self.tickRate );
14103                self.isPaused = false;
14104                self.trigger( "resume" );
14105            } else if ( self._total > 0 ){
14106                self.start( self._total / 1000 );
14107            }
14108        },
14109        onTick: function(){
14110            const self = this;
14111            const diff = self._total - Math.max( self._target - Date.now(), 0 );
14112            self.percent = Math.min( ( diff / self._total ) * 100, 100 );
14113            self.trigger( "tick", [ self.percent ] );
14114            if ( self.percent >= 100 ){
14115                if ( self._intervalId !== null ) clearInterval( self._intervalId );
14116                self._intervalId = null;
14117                self._target = null;
14118                self.isActive = false;
14119                self.trigger( "complete" );
14120            }
14121        }
14122    } );
14123
14124    /**
14125     * @memberof FooGallery.
14126     * @class Carousel
14127     * @extends FooGallery.utils.Class
14128     */
14129    _.Carousel = _utils.Class.extend( /** @lends FooGallery.Carousel.prototype */ {
14130        /**
14131         * @ignore
14132         * @constructs
14133         * @param {FooGallery.Template} template
14134         * @param {object} opt
14135         * @param {object} cls
14136         * @param {object} sel
14137         */
14138        construct: function(template, opt, cls, sel, i18n){
14139            const self = this;
14140            self.tmpl = template;
14141            self.el = template.el;
14142            self.opt = opt;
14143            self.cls = cls;
14144            self.sel = sel;
14145            self.i18n = i18n;
14146            self.elem = {
14147                inner: null,
14148                center: null,
14149                bottom: null,
14150                prev: null,
14151                next: null,
14152                progress: null
14153            };
14154            self.activeItem = null;
14155            self._itemWidth = 0;
14156            self._leftExclude = [ self.sel.activeItem, self.sel.nextItem ].join( "," );
14157            self._rightExclude = [ self.sel.activeItem, self.sel.prevItem ].join( "," );
14158            /**
14159             *
14160             * @type {FooGallery.utils.DOMEventListeners}
14161             * @private
14162             */
14163            self._centerListeners = new _utils.DOMEventListeners();
14164            /**
14165             *
14166             * @type {FooGallery.utils.DOMEventListeners}
14167             * @private
14168             */
14169            self._listeners = new _utils.DOMEventListeners();
14170            self._generatedControls = [];
14171            /**
14172             *
14173             * @type {FooGallery.utils.Progress}
14174             * @private
14175             */
14176            self._progress = new _utils.Progress();
14177            self._canHover = window.matchMedia("(hover: hover)").matches;
14178            self.cache = new Map();
14179            self.timeouts = new _utils.Timeouts();
14180            self.interacted = false;
14181            self.isRTL = false;
14182            self._firstLayout = true;
14183        },
14184
14185        //#region Transform Utils
14186
14187        /**
14188         * @summary Calculate the screen X co-ord given a 3D points' X, Z co-ords and the perspective.
14189         * @memberof
14190         * @param {number} vectorX - The 3D point X co-ord.
14191         * @param {number} vectorZ - The 3D point Z co-ord.
14192         * @param {number} perspective - The visual perspective.
14193         * @returns {number} The screen X co-ord.
14194         */
14195        getScreenX: function( vectorX, vectorZ, perspective ){
14196            return ( perspective / ( perspective + vectorZ ) ) * vectorX;
14197        },
14198        /**
14199         * @summary Calculate a 3D points' X co-ord given the screens' X co-ord, the 3D points' Z co-ord and the perspective.
14200         * @param {number} screenX - The screen X co-ord.
14201         * @param {number} vectorZ - The 3D point Z co-ord.
14202         * @param {number} perspective - The visual perspective.
14203         * @returns {number} The 3D point X co-ord.
14204         */
14205        getVectorX: function( screenX, vectorZ, perspective ){
14206            return ( ( perspective + vectorZ ) / perspective ) * screenX;
14207        },
14208        /**
14209         * @summary Calculate the incremental Z co-ord for a 3D point that is part of a scaled sequence.
14210         * @param {number} index - The index within the sequence for this iteration.
14211         * @param {number} scale - The scale to adjust each iteration by.
14212         * @param {number} perspective - The visual perspective.
14213         * @returns {number} The 3D point Z co-ord
14214         */
14215        getSequentialZFromScale: function( index, scale, perspective ){
14216            return perspective * ( scale * ( index + 1 ) );
14217        },
14218        /**
14219         * @summary Scales the value to the given 3D points' Z co-ord and perspective.
14220         * @param {number} value - The value to be scaled.
14221         * @param {number} vectorZ - The 3D point Z co-ord.
14222         * @param {number} perspective - The visual perspective.
14223         * @returns {number} The scaled value.
14224         */
14225        scaleToZ: function( value, vectorZ, perspective ){
14226            return value * ( 1 - vectorZ / ( perspective + vectorZ ) );
14227        },
14228        /**
14229         * @summary Returns the absolute difference between 2 numbers.
14230         * @param {number} num1
14231         * @param {number} num2
14232         * @returns {number}
14233         */
14234        getDiff: function( num1, num2 ){
14235            return num1 > num2 ? num1 - num2 : num2 - num1;
14236        },
14237
14238        //#endregion
14239
14240        //#region Autoplay
14241
14242        pause: function(){
14243            this._progress.pause();
14244        },
14245        resume: function(){
14246            this._progress.resume();
14247        },
14248        start: function(){
14249            if ( this.opt.autoplay.interaction === "disable" && this.interacted ) return;
14250            this._progress.start( this.opt.autoplay.time );
14251        },
14252        stop: function(){
14253            this._progress.stop();
14254        },
14255
14256        //#endregion
14257
14258        init: function(){
14259            const self = this;
14260            self.isRTL = window.getComputedStyle(self.el).direction === "rtl";
14261
14262            self.elem = {
14263                inner: self.el.querySelector( self.sel.inner ),
14264                center: self.el.querySelector( self.sel.center ),
14265                bottom: self.el.querySelector( self.sel.bottom ),
14266                prev: self.el.querySelector( self.sel.prev ),
14267                next: self.el.querySelector( self.sel.next ),
14268                progress: self.el.querySelector( self.sel.progress )
14269            };
14270
14271            if ( self.opt.perspective !== 150 ){
14272                self.el.style.setProperty( "--fg-carousel-perspective", self.opt.perspective + "px" );
14273            }
14274        },
14275        postInit: function(){
14276            const self = this;
14277            self.activeItem = self.tmpl.items.first();
14278            self.initNavigation();
14279            self.initPagination();
14280            self.initSwipe();
14281            self.initAutoplay();
14282        },
14283        initNavigation: function(){
14284            const self = this;
14285            self._listeners.add( self.elem.prev, "click", function( event ){
14286                event.preventDefault();
14287                self.interacted = true;
14288                self.previous();
14289            } );
14290            if ( self.elem.prev.type !== "button" ) self.elem.prev.type = "button";
14291            const prevIcon = _icons.element( "arrow-left" );
14292            self._generatedControls.push( prevIcon );
14293            self.elem.prev.appendChild( prevIcon );
14294            if ( !self.elem.prev.title?.length ) {
14295                self.elem.prev.title = self.i18n.prev;
14296            }
14297
14298            self._listeners.add( self.elem.next, "click", function( event ){
14299                event.preventDefault();
14300                self.interacted = true;
14301                self.next();
14302            } );
14303            if ( self.elem.next.type !== "button" ) self.elem.next.type = "button";
14304            const nextIcon = _icons.element( "arrow-right" );
14305            self._generatedControls.push( nextIcon );
14306            self.elem.next.appendChild( nextIcon );
14307            if ( !self.elem.next.title?.length ) {
14308                self.elem.next.title = self.i18n.next;
14309            }
14310        },
14311        getBulletTitle: function( str, num ) {
14312            return str.replace( /\{ITEM}/g, `${ num }` );
14313        },
14314        initPagination: function(){
14315            const self = this;
14316            const count = self.tmpl.items.count();
14317            for (let i = 0; i < count; i++){
14318                const bullet = document.createElement( "button" );
14319                bullet.type = "button";
14320                bullet.classList.add( self.cls.bullet );
14321                bullet.title = self.getBulletTitle( self.i18n.bullet, i + 1 );
14322                if ( i === 0 ){
14323                    bullet.classList.add( self.cls.activeBullet );
14324                    bullet.title = self.getBulletTitle( self.i18n.activeBullet, i + 1 );
14325                }
14326                self._listeners.add( bullet, "click", function( event ){
14327                    event.preventDefault();
14328                    self.interacted = true;
14329                    self.goto( self.tmpl.items.get( i ) );
14330                } );
14331                self._generatedControls.push( bullet );
14332                self.elem.bottom.appendChild( bullet );
14333            }
14334        },
14335        initSwipe: function(){
14336            const self = this;
14337            let startX = 0, endX = 0, min = 25 * (window.devicePixelRatio || 1);
14338            self._listeners.add( self.elem.inner, "touchstart", function( event ){
14339                self.interacted = true;
14340                startX = event.changedTouches[0].screenX;
14341            }, { passive: true } );
14342
14343            self._listeners.add( self.elem.inner, "touchend", function( event ){
14344                endX = event.changedTouches[0].screenX;
14345                const diff = self.getDiff( startX, endX );
14346                if ( diff > min ){
14347                    if ( endX < startX ){ // swipe left
14348                        self.next();
14349                    } else { // swipe right
14350                        self.previous();
14351                    }
14352                }
14353                endX = 0;
14354                startX = 0;
14355            }, { passive: true } );
14356        },
14357        initAutoplay: function(){
14358            const self = this, opt = self.opt.autoplay;
14359            if ( opt.time <= 0 || !( self.elem.progress instanceof HTMLElement ) ) return;
14360
14361            const progressElement = self.elem.progress.style;
14362
14363            let frame = null;
14364            function update( percent, immediate ){
14365                _utils.cancelFrame( frame );
14366                _utils.requestFrame( function(){
14367                    progressElement.setProperty( "width", percent + "%" );
14368                    if ( immediate ){
14369                        progressElement.setProperty( "transition-duration", "0s" );
14370                    } else {
14371                        progressElement.setProperty( "transition-duration", self._progress.tickRate + "ms" );
14372                    }
14373                } );
14374            }
14375
14376            self._progress.on( {
14377                "start stop": function(){
14378                    update( 0, true );
14379                },
14380                "pause resume": function(){
14381                    update( self._progress.percent, true );
14382                },
14383                "tick": function() {
14384                    update( self._progress.percent, false );
14385                },
14386                "complete": function() {
14387                    self.next( function(){
14388                        self._progress.start( opt.time );
14389                    } );
14390                }
14391            } );
14392
14393            if ( opt.interaction === "pause" ){
14394                if ( self._canHover ) {
14395                    self._listeners.add( self.el, "mouseenter", function( event ) {
14396                        self._progress.pause();
14397                    }, { passive: true } );
14398                    self._listeners.add( self.el, "mouseleave", function( event ) {
14399                        self._progress.resume();
14400                    }, { passive: true } );
14401                } else {
14402                    // handle touch only
14403                    self._listeners.add( self.el, "touchstart", function( event ) {
14404                        self.timeouts.delete( "autoplay" );
14405                        self._progress.pause();
14406                    }, { passive: true } );
14407
14408                    self._listeners.add( self.el, "touchend", function( event ) {
14409                        self.timeouts.set( "autoplay", function(){
14410                            self._progress.resume();
14411                        }, opt.time * 1000 );
14412                    }, { passive: true } );
14413                }
14414            }
14415            self._progress.start( opt.time );
14416        },
14417        getFirst: function(){
14418            return this.tmpl.items.first();
14419        },
14420        getNext: function( item ){
14421            return this.tmpl.items.next( !( item instanceof _.Item ) ? this.activeItem : item, null, true );
14422        },
14423        getPrev: function( item ){
14424            return this.tmpl.items.prev( !( item instanceof _.Item ) ? this.activeItem : item, null, true );
14425        },
14426        goto: function( item, callback ){
14427            const self = this;
14428            if ( !( item instanceof _.Item ) ){
14429                return;
14430            }
14431            const autoplay = self.opt.autoplay;
14432            const restart = !self._canHover && self.timeouts.has( "autoplay" );
14433            self.timeouts.delete( "autoplay" );
14434            self.timeouts.delete( "navigation" );
14435
14436            const pause = self._progress.isPaused;
14437            if ( self._progress.isActive ){
14438                self._progress.stop();
14439            }
14440
14441            self.activeItem = item;
14442            self.layout();
14443
14444            self.timeouts.set( "navigation", function(){
14445                if ( autoplay.time > 0 && ( autoplay.interaction === "pause" || ( autoplay.interaction === "disable" && !self.interacted ) ) ){
14446                    self._progress.start( autoplay.time );
14447                    if ( pause ){
14448                        self._progress.pause();
14449                    }
14450                    if ( restart ){
14451                        self.timeouts.set( "autoplay", function(){
14452                            self._progress.resume();
14453                        }, autoplay.time * 1000 );
14454                    }
14455                }
14456                if ( _is.fn( callback ) ) callback.call( self );
14457            }, self.opt.speed );
14458        },
14459        next: function( callback ){
14460            this.goto( this.getNext(), callback );
14461        },
14462        previous: function( callback ){
14463            this.goto( this.getPrev(), callback );
14464        },
14465        destroy: function(){
14466            const self = this;
14467            self.cache.clear();
14468            self.timeouts.clear();
14469            self._listeners.clear();
14470            self._centerListeners.clear();
14471            self._progress.destroy();
14472            // Breakpoint rebuilds reuse the markup, so remove this instance's controls.
14473            self._generatedControls.forEach( function( element ){
14474                element.remove();
14475            } );
14476            self._generatedControls = [];
14477            if ( self.opt.perspective !== 150 ){
14478                self.el.style.removeProperty( "--fg-carousel-perspective" );
14479            }
14480        },
14481        getSize: function( element, inner ){
14482            const rect = element.getBoundingClientRect();
14483            const size = { width: rect.width, height: rect.height };
14484            if ( inner ){
14485                const style = getComputedStyle( element );
14486                size.width -= parseFloat( style.paddingLeft ) + parseFloat( style.paddingRight ) + parseFloat( style.borderLeftWidth ) + parseFloat( style.borderRightWidth );
14487                size.height -= parseFloat( style.paddingTop ) + parseFloat( style.paddingBottom ) + parseFloat( style.borderTopWidth ) + parseFloat( style.borderBottomWidth );
14488            }
14489            return size;
14490        },
14491        layout: function( width ){
14492            const self = this;
14493            if ( self.activeItem === null ){
14494                self.activeItem = self.tmpl.items.first();
14495            }
14496            if ( !_is.number( width ) && self.cache.has( "width" ) ){
14497                width = self.cache.get( "width" );
14498            }
14499            if ( !_is.number( width ) ) return;
14500
14501            const layout = self.getLayout( width );
14502            if ( self._layoutFrame !== null ) _utils.cancelFrame( self._layoutFrame );
14503            self._layoutFrame = _utils.requestFrame( function(){
14504                self._layoutFrame = null;
14505                if ( self.renderActive( layout ) ){
14506                    self.renderSide( "left", self.sel.prevItem, self._leftExclude, self.cls.prevItem, layout );
14507                    self.renderSide( "right", self.sel.nextItem, self._rightExclude, self.cls.nextItem, layout );
14508                    self._firstLayout = false;
14509                }
14510            } );
14511        },
14512        getLayout: function( width ){
14513            const self = this;
14514            const activePosition = self.getActivePosition();
14515            const align = self.getAlign();
14516            const usePositionedLayout = activePosition !== "center";
14517            const mode = activePosition + "|" + align;
14518
14519            if ( self.cache.has("layout") && self.cache.get("width") === width && self.cache.get("layoutMode") === mode ){
14520                return self.cache.get("layout");
14521            }
14522            const itemWidth = self.getSize( self.elem.center ).width;
14523            const innerWidth = self.getSize( self.elem.inner, true ).width;
14524            const maxOffset = ( innerWidth / 2 ) + ( itemWidth / 2 );
14525            const layout = self.calculate( itemWidth, maxOffset );
14526
14527            layout.activeX = 0;
14528            layout.sideBySide = null;
14529
14530            if ( usePositionedLayout ) {
14531                const visualLeftSide = self.isRTL ? "right" : "left";
14532                const visualRightSide = self.isRTL ? "left" : "right";
14533                const dominantSide = activePosition === "start" ? visualRightSide : visualLeftSide;
14534                let showCount = Math.max( 0, self.getShowPerSide() * 2 );
14535                let positionedLayout = null;
14536                let sideLayout = [];
14537                let fit = { width: itemWidth, center: 0 };
14538
14539                // maxItems is a cap; reduce visible items until the positioned sequence fits inner width.
14540                while ( showCount >= 0 ){
14541                    positionedLayout = self.calculate(
14542                        itemWidth,
14543                        innerWidth + ( itemWidth / 2 ),
14544                        self.opt.gutter.max,
14545                        showCount
14546                    );
14547                    sideLayout = positionedLayout.side;
14548                    fit = self.measureAlignedFit( itemWidth, sideLayout, dominantSide );
14549                    if ( fit.width <= innerWidth ){
14550                        break;
14551                    }
14552                    showCount -= 1;
14553                }
14554
14555                // Keep the displayed sequence centered while the active item stays at the chosen position.
14556                layout.activeX = -fit.center;
14557                layout.sideBySide = {};
14558                layout.sideBySide.left = [];
14559                layout.sideBySide.right = [];
14560                layout.sideBySide[dominantSide] = sideLayout;
14561                if ( positionedLayout && _is.number( positionedLayout.zIndex ) ){
14562                    layout.zIndex = positionedLayout.zIndex;
14563                }
14564            }
14565
14566            self.applyVisualAlign( layout, itemWidth, innerWidth );
14567
14568            self.cache.set( "width", width );
14569            self.cache.set( "layoutMode", mode );
14570            if ( layout?.side?.length > 0 ) {
14571                self.cache.set( "layout", layout );
14572            }
14573            return layout;
14574        },
14575        getActivePosition: function(){
14576            const value = _is.string( this.opt.activePosition )
14577                ? this.opt.activePosition.toLowerCase()
14578                : "center";
14579            return [ "start", "end" ].indexOf( value ) !== -1 ? value : "center";
14580        },
14581        getAlign: function(){
14582            const value = _is.string( this.opt.align )
14583                ? this.opt.align.toLowerCase()
14584                : "center";
14585            return [ "left", "right" ].indexOf( value ) !== -1 ? value : "center";
14586        },
14587        applyVisualAlign: function( layout, itemWidth, innerWidth ){
14588            const self = this;
14589            const align = self.getAlign();
14590            if ( align === "center" ) return;
14591
14592            const bounds = self.measureVisibleBounds( itemWidth, layout );
14593            const target = align === "left"
14594                ? -( innerWidth / 2 )
14595                : ( innerWidth / 2 );
14596            const delta = align === "left"
14597                ? target - bounds.min
14598                : target - bounds.max;
14599
14600            layout.activeX = ( _is.number( layout.activeX ) ? layout.activeX : 0 ) + delta;
14601        },
14602        measureVisibleBounds: function( itemWidth, layout ){
14603            const self = this;
14604            const activeX = _is.number( layout?.activeX ) ? layout.activeX : 0;
14605            const leftValues = layout?.sideBySide?.left || layout.side || [];
14606            const rightValues = layout?.sideBySide?.right || layout.side || [];
14607            const result = {
14608                min: activeX - ( itemWidth / 2 ),
14609                max: activeX + ( itemWidth / 2 )
14610            };
14611            const perspective = self.opt.perspective;
14612            const update = function( side, values ){
14613                for ( let i = 0; i < values.length; i++ ){
14614                    const entry = values[i];
14615                    const signedX = ( side === "left" && !self.isRTL ) || ( side === "right" && self.isRTL ) ? -entry.x : entry.x;
14616                    const centerX = self.getScreenX( signedX, entry.z, perspective ) + activeX;
14617                    const width = self.scaleToZ( itemWidth, entry.z, perspective );
14618                    const min = centerX - ( width / 2 );
14619                    const max = centerX + ( width / 2 );
14620                    if ( min < result.min ) result.min = min;
14621                    if ( max > result.max ) result.max = max;
14622                }
14623            };
14624
14625            update( "left", leftValues );
14626            update( "right", rightValues );
14627
14628            return result;
14629        },
14630        measureAlignedFit: function( itemWidth, sideLayout, dominantSide ){
14631            const self = this;
14632            const result = {
14633                min: -( itemWidth / 2 ),
14634                max: itemWidth / 2,
14635                width: itemWidth,
14636                center: 0
14637            };
14638
14639            for ( let i = 0; i < sideLayout.length; i++ ){
14640                const values = sideLayout[i];
14641                const signedX = ( dominantSide === "left" && !self.isRTL ) || ( dominantSide === "right" && self.isRTL ) ? -values.x : values.x;
14642                const centerX = self.getScreenX( signedX, values.z, self.opt.perspective );
14643                const width = self.scaleToZ( itemWidth, values.z, self.opt.perspective );
14644                const left = centerX - ( width / 2 );
14645                const right = centerX + ( width / 2 );
14646                if ( left < result.min ) result.min = left;
14647                if ( right > result.max ) result.max = right;
14648            }
14649
14650            result.width = result.max - result.min;
14651            result.center = ( result.min + result.max ) / 2;
14652
14653            return result;
14654        },
14655        round: function( value, precision ){
14656            let multiplier = Math.pow(10, precision || 0);
14657            return Math.round(value * multiplier) / multiplier;
14658        },
14659        getShowPerSide: function(){
14660            const self = this, count = self.tmpl.items.count();
14661            let perSide = self.opt.maxItems;
14662            if ( perSide === "auto" || perSide <= 0 ) perSide = ( count % 2 === 0 ? count - 1 : count );
14663            if ( perSide % 2 === 0 ) perSide -= 1;
14664            if ( perSide < 1 ) perSide = 1;
14665            if ( perSide > count ) perSide = ( count % 2 === 0 ? count - 1 : count );
14666            return ( perSide - 1 ) / 2;
14667        },
14668        calculate: function( itemWidth, maxOffset, gutter, showPerSide ){
14669            const self = this;
14670            if ( !_is.number( gutter ) ){
14671                gutter = self.opt.gutter.max;
14672            }
14673            if ( !_is.number( showPerSide ) ){
14674                showPerSide = self.getShowPerSide();
14675            }
14676
14677            const result = {
14678                zIndex: showPerSide + 10,
14679                gutter: gutter,
14680                perSide: showPerSide,
14681                side: []
14682            };
14683
14684            if ( itemWidth > 0 ) {
14685                let offset = itemWidth, zIndex = result.zIndex - 1;
14686                for (let i = 0; i < showPerSide; i++, zIndex--){
14687                    const z = self.getSequentialZFromScale( i, self.opt.scale, self.opt.perspective );
14688                    const width = self.scaleToZ( itemWidth, z, self.opt.perspective );
14689                    const diff = ( itemWidth - width ) / 2;
14690
14691                    offset -= diff;
14692
14693                    const gutterStep = 1;
14694                    const gutterValue = self.opt.gutter.unit === "%" ? itemWidth * Math.abs( gutter / 100 ) : Math.abs( gutter );
14695                    const gutterOffset = self.opt.gutter.unit === "%" ? self.scaleToZ( gutterValue, z, self.opt.perspective ) : gutterValue;
14696                    if (gutter > 0){
14697                        offset += gutterOffset;
14698                    } else {
14699                        offset -= gutterOffset;
14700                    }
14701                    if ( offset + width + diff > maxOffset ){
14702                        if ( gutter - gutterStep < self.opt.gutter.min ){
14703                            return self.calculate( itemWidth, maxOffset, self.opt.gutter.max, showPerSide - 1);
14704                        }
14705                        return self.calculate( itemWidth, maxOffset, gutter - gutterStep, showPerSide);
14706                    }
14707
14708                    const x = self.getVectorX( offset, z, self.opt.perspective );
14709
14710                    offset += width + diff;
14711
14712                    result.side.push({x: x, z: z, zIndex: zIndex });
14713                }
14714            }
14715            return result;
14716        },
14717        cleanup: function( selector, className, exclude ){
14718            const self = this;
14719            const hasExclude = _is.string( exclude );
14720            Array.from( self.el.querySelectorAll( selector ) ).forEach( function( node ){
14721
14722                node.classList.remove( className );
14723                if ( self.opt.centerOnClick ){
14724                    self._centerListeners.remove( node, "click" );
14725                }
14726
14727                if ( hasExclude && node.matches( exclude ) ) return;
14728
14729                node.style.removeProperty( "transition-duration" );
14730                node.style.removeProperty( "transform" );
14731                node.style.removeProperty( "z-index" );
14732            } );
14733        },
14734        renderActive: function( layout ){
14735            const self = this;
14736            if ( ! ( self.activeItem instanceof _.Item ) ) return false;
14737            const el = self.activeItem.el;
14738
14739            self.cleanup( self.sel.activeItem, self.cls.activeItem );
14740
14741            el.classList.add( self.cls.activeItem );
14742            el.style.setProperty("transition-duration", ( self._firstLayout ? 0 : self.opt.speed ) + "ms" );
14743            el.style.setProperty( "z-index", layout.zIndex );
14744            if ( _is.number( layout.activeX ) && layout.activeX !== 0 ){
14745                el.style.setProperty( "transform", "translate3d(" + layout.activeX + "px, 0, 0)" );
14746            } else {
14747                el.style.removeProperty( "transform" );
14748            }
14749
14750            const ai = self.tmpl.items && _is.fn( self.tmpl.items.indexOf ) ? self.tmpl.items.indexOf( self.activeItem ) : -1;
14751            Array.from( self.el.querySelectorAll( self.sel.bullet ) ).forEach( function( node, i ){
14752                node.classList.remove( self.cls.activeBullet );
14753                node.title = self.getBulletTitle( self.i18n.bullet, i + 1 );
14754                if ( i === ai ){
14755                    node.classList.add( self.cls.activeBullet );
14756                    node.title = self.getBulletTitle( self.i18n.activeBullet, i + 1 );
14757                }
14758            } );
14759            return true;
14760        },
14761        renderSide: function( side, selector, exclude, cls, layout ) {
14762            const self = this;
14763            if ( [ "left","right" ].indexOf( side ) === -1 ) return;
14764
14765            self.cleanup( selector, cls, exclude );
14766
14767            const valuesList = layout?.sideBySide?.[side] || layout.side;
14768            const activeX = _is.number( layout?.activeX ) ? layout.activeX : 0;
14769            let place = self.activeItem;
14770            for (let i = 0; i < valuesList.length; i++ ){
14771                const values = valuesList[i];
14772                const item = side === "left" ? self.getPrev( place ) : self.getNext( place );
14773                if ( item instanceof _.Item ){
14774                    const signedX = ( side === "left" && !self.isRTL ) || ( side === "right" && self.isRTL ) ? -values.x : values.x;
14775                    let vectorX = signedX;
14776
14777                    // Apply active position offset in screen space so perspective math remains bounded.
14778                    if ( activeX !== 0 ){
14779                        const screenX = self.getScreenX( signedX, values.z, self.opt.perspective ) + activeX;
14780                        vectorX = self.getVectorX( screenX, values.z, self.opt.perspective );
14781                    }
14782
14783                    let transform = "translate3d(" + vectorX + "px, 0,-" + values.z + "px)";
14784                    item.el.classList.add( cls );
14785                    if ( !item.isLoaded ){
14786                        item.el.style.setProperty("transition-duration", "0ms" );
14787                        item.el.style.setProperty( "transform", "translate3d(0,0,-" + self.opt.perspective + "px)" );
14788                        item.el.offsetHeight;
14789                    }
14790                    item.el.style.setProperty("transition-duration", ( self._firstLayout ? 0 : self.opt.speed ) + "ms" );
14791                    item.el.style.setProperty( "transform", transform );
14792                    item.el.style.setProperty( "z-index", values.zIndex );
14793
14794                    if ( self.opt.centerOnClick ){
14795                        self._centerListeners.add( item.el, "click", function( event ){
14796                            event.preventDefault();
14797                            event.stopPropagation();
14798                            self.interacted = true;
14799                            self.goto( item );
14800                        }, true );
14801                    }
14802
14803                    place = item;
14804                } else {
14805                    // exit early if no item was found
14806                    break;
14807                }
14808            }
14809        }
14810    });
14811
14812})(
14813    FooGallery.$,
14814    FooGallery,
14815    FooGallery.icons,
14816    FooGallery.utils,
14817    FooGallery.utils.is
14818);
14819
14820(function($, _, _obj){
14821
14822    _.CarouselTemplate = _.Template.extend({
14823        construct: function(options, element){
14824            const self = this;
14825            self._super(_obj.extend({}, options, {
14826                paging: {
14827                    type: "none"
14828                }
14829            }), element);
14830            self.items.LAYOUT_AFTER_LOAD = false;
14831            self.carousel = null;
14832            self.on({
14833                "pre-init": self.onPreInit,
14834                "init": self.onInit,
14835                "post-init": self.onPostInit,
14836                "destroyed": self.onDestroyed,
14837                "append-item": self.onAppendItem,
14838                "after-filter-change": self.onAfterFilterChange,
14839                "layout": self.onLayout
14840            }, self);
14841            if ( !!_.Panel && self.lightbox instanceof _.Panel ){
14842                self.lightbox.on({
14843                    "open": self.onLightboxOpen,
14844                    "closed": self.onLightboxClosed,
14845                    "next": self.onLightboxNext,
14846                    "prev": self.onLightboxPrev
14847                }, self);
14848            }
14849        },
14850        onPreInit: function(){
14851            const self = this;
14852            self.carousel = new _.Carousel( self, self.template, self.cls.carousel, self.sel.carousel, self.il8n );
14853        },
14854        onInit: function(){
14855            this.carousel.init();
14856        },
14857        onPostInit: function(){
14858            this.carousel.postInit();
14859        },
14860        onDestroyed: function(){
14861            const self = this;
14862            if (self.carousel instanceof _.Carousel){
14863                self.carousel.destroy();
14864            }
14865        },
14866        onAppendItem: function (event, item) {
14867            event.preventDefault();
14868            this.carousel.elem.inner.appendChild(item.el);
14869            item.isAttached = true;
14870        },
14871        onAfterFilterChange: function(){
14872            this.carousel.cache.delete( "layout" );
14873            this.carousel.interacted = true;
14874            this.carousel.goto(this.carousel.getFirst());
14875        },
14876        onLayout: function(){
14877            this.carousel.layout(this.lastWidth);
14878        },
14879        onLightboxOpen: function(){
14880            this.carousel.interacted = true;
14881            this.carousel.stop();
14882        },
14883        onLightboxClosed: function(){
14884            this.carousel.start();
14885        },
14886        onLightboxNext: function(){
14887            this.carousel.next();
14888        },
14889        onLightboxPrev: function(){
14890            this.carousel.previous();
14891        }
14892    });
14893
14894    _.template.register("carousel", _.CarouselTemplate, {
14895        template: {
14896            maxItems: 0, // "auto" or 0 will be calculated on the fly.
14897            perspective: 150,
14898            scale: 0.12,
14899            speed: 300,
14900            align: "center", // "center", "left" or "right". Visual alignment of the entire visible item set.
14901            activePosition: "center", // "center", "start" or "end". Controls active item position in the visible sequence.
14902            centerOnClick: true,
14903            gutter: {
14904                min: -40,
14905                max: -20,
14906                unit: "%"
14907            },
14908            autoplay: {
14909                time: 0,
14910                interaction: "pause" // "pause" or "disable"
14911            }
14912        }
14913    }, {
14914        container: "foogallery fg-carousel",
14915        carousel: {
14916            inner: "fg-carousel-inner",
14917            center: "fg-carousel-center",
14918            bottom: "fg-carousel-bottom",
14919            prev: "fg-carousel-prev",
14920            next: "fg-carousel-next",
14921            bullet: "fg-carousel-bullet",
14922            activeBullet: "fg-bullet-active",
14923            activeItem: "fg-item-active",
14924            prevItem: "fg-item-prev",
14925            nextItem: "fg-item-next",
14926            progress: "fg-carousel-progress"
14927        }
14928    }, {
14929        prev: "Previous",
14930        next: "Next",
14931        bullet: "Item {ITEM}",
14932        activeBullet: "Item {ITEM} - Current"
14933    });
14934
14935})(
14936    FooGallery.$,
14937    FooGallery,
14938    FooGallery.utils.obj
14939);

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.