PageSourceSearch

https://www.thomasmemoriallibrary.org/wp-content/plugins/foogaller…/assets/js/foogallery.68b5b3a7.js?ver=3.3.7

js thomasmemoriallibrary.org collected 2026-10-04 13:15:45 UTC 674,699 bytes, 17,933 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($, _, _utils, _is){
8944
8945	_.Video = _.Item.extend({
8946		construct: function(template, options){
8947			var self = this;
8948			self._super(template, options);
8949			self.cover = self.opt.cover;
8950		},
8951		doParseItem: function($element){
8952			var self = this;
8953			if (self._super($element)){
8954				self.cover = self.$anchor.data("cover") || self.cover;
8955				return true;
8956			}
8957			return false;
8958		},
8959		doCreateItem: function(){
8960			var self = this;
8961			if (self._super()){
8962				self.$anchor.attr("data-cover", self.cover);
8963				return true;
8964			}
8965			return false;
8966		},
8967		toJSON: function(){
8968			var json = this._super();
8969			json.cover = this.cover;
8970			return json;
8971		}
8972	});
8973
8974	_.template.configure("core", {
8975		item: {
8976			cover: ""
8977		}
8978	},{
8979		item: {
8980			types: {
8981				video: "fg-type-video"
8982			}
8983		}
8984	});
8985
8986	_.components.register("video", _.Video);
8987
8988})(
8989		FooGallery.$,
8990		FooGallery,
8991		FooGallery.utils,
8992		FooGallery.utils.is
8993);
8994(function($, _, _utils, _is){
8995
8996	_.Iframe = _.Item.extend({});
8997
8998	_.template.configure("core", null,{
8999		item: {
9000			types: {
9001				iframe: "fg-type-iframe"
9002			}
9003		}
9004	});
9005
9006	_.components.register("iframe", _.Iframe);
9007
9008})(
9009		FooGallery.$,
9010		FooGallery,
9011		FooGallery.utils,
9012		FooGallery.utils.is
9013);
9014(function($, _, _utils, _is){
9015
9016	_.Html = _.Item.extend({});
9017
9018	_.template.configure("core", null,{
9019		item: {
9020			types: {
9021				html: "fg-type-html"
9022			}
9023		}
9024	});
9025
9026	_.components.register("html", _.Html);
9027
9028})(
9029		FooGallery.$,
9030		FooGallery,
9031		FooGallery.utils,
9032		FooGallery.utils.is
9033);
9034(function($, _, _utils, _is){
9035
9036	_.Embed = _.Video.extend({});
9037
9038	_.template.configure("core", null,{
9039		item: {
9040			types: {
9041				embed: "fg-type-embed fg-type-video"
9042			}
9043		}
9044	});
9045
9046	_.components.register("embed", _.Embed);
9047
9048})(
9049		FooGallery.$,
9050		FooGallery,
9051		FooGallery.utils,
9052		FooGallery.utils.is
9053);
9054(function(_, _is, _fn, _obj){
9055
9056	_.FilteringFactory = _.Factory.extend(/** @lends FooGallery.FilteringFactory */{
9057		/**
9058		 * @summary A factory for filtering types allowing them to be easily registered and created.
9059		 * @memberof FooGallery
9060		 * @constructs FilteringFactory
9061		 * @description The plugin makes use of an instance of this class exposed as {@link FooGallery.filtering}.
9062		 * @augments FooGallery.Factory
9063		 * @borrows FooGallery.utils.Class.extend as extend
9064		 * @borrows FooGallery.utils.Class.override as override
9065		 */
9066		construct: function(){
9067			/**
9068			 * @summary An object containing all registered filtering types.
9069			 * @memberof FooGallery.FilteringFactory#
9070			 * @name registered
9071			 * @type {Object.<string, Object>}
9072			 * @readonly
9073			 * @example {@caption The following shows the structure of this object. The `<name>` placeholders would be the name the class was registered with.}
9074			 * {
9075			 * 	"<name>": {
9076			 * 		"name": <string>,
9077			 * 		"klass": <function>,
9078			 * 		"ctrl": <function>,
9079			 * 		"priority": <number>
9080			 * 	},
9081			 * 	"<name>": {
9082			 * 		"name": <string>,
9083			 * 		"klass": <function>,
9084			 * 		"ctrl": <function>,
9085			 * 		"priority": <number>
9086			 * 	},
9087			 * 	...
9088			 * }
9089			 */
9090			this.registered = {};
9091		},
9092		/**
9093		 * @summary Registers a filtering `type` constructor with the factory using the given `name` and `test` function.
9094		 * @memberof FooGallery.FilteringFactory#
9095		 * @function register
9096		 * @param {string} name - The friendly name of the class.
9097		 * @param {FooGallery.Filtering} type - The filtering type constructor to register.
9098		 * @param {FooGallery.FilteringControl} [ctrl] - An optional control to register for the filtering type.
9099		 * @param {object} [options={}] - The default options for the filtering type.
9100		 * @param {object} [classes={}] - The CSS classes for the filtering type.
9101		 * @param {object} [il8n={}] - The il8n strings for the filtering type.
9102		 * @param {number} [priority=0] - This determines the index for the class when using either the {@link FooGallery.FilteringFactory#load|load} or {@link FooGallery.FilteringFactory#names|names} methods, a higher value equals a lower index.
9103		 * @returns {boolean} `true` if the `klass` was successfully registered.
9104		 */
9105		register: function(name, type, ctrl, options, classes, il8n, priority){
9106			var self = this, result = self._super(name, type, priority);
9107			if (result){
9108				var reg = self.registered;
9109				reg[name].ctrl = _is.fn(ctrl) ? ctrl : null;
9110				reg[name].opt = _is.hash(options) ? options : {};
9111				reg[name].cls = _is.hash(classes) ? classes : {};
9112				reg[name].il8n = _is.hash(il8n) ? il8n : {};
9113			}
9114			return result;
9115		},
9116		type: function(options){
9117			var self = this, opt;
9118			return _is.hash(options) && _is.hash(opt = options.filtering) && _is.string(opt.type) && self.contains(opt.type) ? opt.type : null;
9119		},
9120		merge: function(options){
9121			options = _obj.extend({}, options);
9122			var self = this, type = self.type(options),
9123				reg = self.registered,
9124				def = reg["default"].opt,
9125				def_cls = reg["default"].cls,
9126				def_il8n = reg["default"].il8n,
9127				opt = _is.hash(options.filtering) ? options.filtering : {},
9128				cls = _is.hash(options.cls) && _is.hash(options.cls.filtering) ? _obj.extend({}, options.cls.filtering) : {},
9129				il8n = _is.hash(options.il8n) && _is.hash(options.il8n.filtering) ? _obj.extend({}, options.il8n.filtering) : {};
9130
9131			if (!_is.hash(options.cls)) options.cls = {};
9132			if (!_is.hash(options.il8n)) options.il8n = {};
9133			if (type !== "default" && self.contains(type)){
9134				options.filtering = _obj.extend({}, def, reg[type].opt, opt, {type: type});
9135				options.cls = _obj.extend(options.cls, {filtering: def_cls}, {filtering: reg[type].cls}, {filtering: cls});
9136				options.il8n = _obj.extend(options.il8n, {filtering: def_il8n}, {filtering: reg[type].il8n}, {filtering: il8n});
9137			} else {
9138				options.filtering = _obj.extend({}, def, opt, {type: type});
9139				options.cls = _obj.extend(options.cls, {filtering: def_cls}, {filtering: cls});
9140				options.il8n = _obj.extend(options.il8n, {filtering: def_il8n}, {filtering: il8n});
9141			}
9142			return options;
9143		},
9144		/**
9145		 * @summary Checks if the factory contains a control registered using the supplied `name`.
9146		 * @memberof FooGallery.FilteringFactory#
9147		 * @function hasCtrl
9148		 * @param {string} name - The friendly name of the class.
9149		 * @returns {boolean}
9150		 */
9151		hasCtrl: function(name){
9152			var self = this, reg = self.registered[name];
9153			return _is.hash(reg) && _is.fn(reg.ctrl);
9154		},
9155		/**
9156		 * @summary Create a new instance of a control class registered with the supplied `name` and arguments.
9157		 * @memberof FooGallery.FilteringFactory#
9158		 * @function makeCtrl
9159		 * @param {string} name - The friendly name of the class.
9160		 * @param {FooGallery.Template} template - The template creating the control.
9161		 * @param {FooGallery.Filtering} parent - The parent filtering class creating the control.
9162		 * @param {string} position - The position the control will be displayed at.
9163		 * @returns {?FooGallery.FilteringControl}
9164		 */
9165		makeCtrl: function(name, template, parent, position){
9166			var self = this, reg = self.registered[name];
9167			if (_is.hash(reg) && _is.fn(reg.ctrl)){
9168				return new reg.ctrl(template, parent, position);
9169			}
9170			return null;
9171		}
9172	});
9173
9174	/**
9175	 * @summary The factory used to register and create the various filtering types of FooGallery.
9176	 * @memberof FooGallery
9177	 * @name filtering
9178	 * @type {FooGallery.FilteringFactory}
9179	 */
9180	_.filtering = new _.FilteringFactory();
9181
9182})(
9183	FooGallery,
9184	FooGallery.utils.is,
9185	FooGallery.utils.fn,
9186	FooGallery.utils.obj
9187);
9188(function ($, _, _utils, _is, _str) {
9189
9190	_.Filtering = _.Component.extend({
9191		construct: function (template) {
9192			var self = this;
9193			/**
9194			 * @ignore
9195			 * @memberof FooGallery.Filtering#
9196			 * @function _super
9197			 */
9198			self._super(template);
9199			self.opt = self.tmpl.opt.filtering;
9200			self.cls = self.tmpl.cls.filtering;
9201			self.il8n = self.tmpl.il8n.filtering;
9202			self.sel = self.tmpl.sel.filtering;
9203			self.pushOrReplace = self.opt.pushOrReplace;
9204			self.type = self.opt.type;
9205			self.theme = self.opt.theme ?? template.getCSSClass('theme');
9206			self.position = self.opt.position;
9207
9208			self.mode = self.opt.mode;
9209			self.sortBy = self.opt.sortBy;
9210			self.sortInvert = self.opt.sortInvert;
9211
9212			self.min = self.opt.min;
9213			self.limit = self.opt.limit;
9214			self.showCount = self.opt.showCount;
9215			self.noAll = self.opt.noAll;
9216			self.autoSelected = self.opt.autoSelected;
9217
9218			self.adjustSize = self.opt.adjustSize;
9219			self.smallest = self.opt.smallest;
9220			self.largest = self.opt.largest;
9221
9222			self.adjustOpacity = self.opt.adjustOpacity;
9223			self.lightest = self.opt.lightest;
9224			self.darkest = self.opt.darkest;
9225
9226			self.current = [];
9227			self.ctrls = [];
9228			self.tags = [];
9229			self.search = '';
9230			self.isMultiLevel = false;
9231		},
9232		/**
9233		 * @summary Parses the hash string into a value used by the plugin.
9234		 * @memberof FooGallery.State#
9235		 * @function fromHash
9236		 * @param {string} hash
9237		 * @return {string[]|string[][]}
9238		 */
9239		fromHash: function(hash){
9240			var self = this, opt = self.tmpl.state.opt;
9241			return hash.split(opt.arraySeparator).map(function(arr){
9242				return _is.empty(arr) ? [] : arr.split(opt.array).map(function(part){
9243					return decodeURIComponent(part);
9244				});
9245			});
9246		},
9247		/**
9248		 * @summary Converts the supplied value into a hash friendly string.
9249		 * @memberof FooGallery.State#
9250		 * @function toHash
9251		 * @param {array|*} value
9252		 * @return {string|null}
9253		 */
9254		toHash: function(value){
9255			var self = this, opt = self.tmpl.state.opt, hash = null;
9256			if (_is.array(value)){
9257				hash = $.map(value, function(tags){
9258					return (_is.array(tags) ? $.map(tags, function(tag){
9259						return _is.undef(tag) ? "" : encodeURIComponent(tag);
9260					}) : []).join(opt.array);
9261				}).join(opt.arraySeparator);
9262			}
9263			return _is.empty(hash) ? null : hash;
9264		},
9265		getState: function(){
9266			return _is.array(this.current) && !this.current.every(function (tags) {
9267				return tags.length === 0;
9268			}) ? this.current.slice() : null;
9269		},
9270		setState: function(state){
9271			var self = this;
9272			self.rebuild();
9273			var toSet = state.filter;
9274			if ( self.autoSelected && ( !_is.array( state.filter ) || state.filter.length === 0 ) && self.tags.length > 0 ){
9275				toSet = [];
9276				for (var i = 0; i < self.tags.length; i++){
9277					if ( !_is.array(self.tags[i]) ) continue;
9278					if ( i === 0 ) toSet.push( [ self.tags[i][0].value ] );
9279					else toSet.push( [] );
9280				}
9281			}
9282			self.set(toSet, "", false);
9283		},
9284		destroy: function () {
9285			var self = this;
9286			self.tags.splice(0, self.tags.length);
9287			$.each(self.ctrls.splice(0, self.ctrls.length), function (i, control) {
9288				control.destroy();
9289			});
9290			self._super();
9291		},
9292		count: function (items, tags) {
9293			items = _is.array(items) ? items : [];
9294			tags = _is.array(tags) ? tags : [];
9295			var result = { __ALL__: 0 }, generate = tags.length === 0;
9296			for (var i = 0, l = items.length, t; i < l; i++) {
9297				if (!_is.empty(t = items[i].tags)) {
9298					result.__ALL__++;
9299					for (var j = 0, jl = t.length, tag; j < jl; j++) {
9300						if (!_is.empty(tag = t[j]) && (generate || (!generate && _utils.inArray(tag, tags) !== -1))) {
9301							if (_is.number(result[tag])) {
9302								result[tag]++;
9303							} else {
9304								result[tag] = 1;
9305							}
9306						}
9307					}
9308				}
9309			}
9310			for (var k = 0, kl = tags.length; k < kl; k++) {
9311				if (!result.hasOwnProperty(tags[k])) result[tags[k]] = 0;
9312			}
9313			return result;
9314		},
9315		createTagObjects: function(items, tags, levelIndex, levelText){
9316			var self = this, result = [];
9317			// first get a count of the tags
9318			var counts = self.count(items, tags), min = Infinity, max = 0, index = -1;
9319			if (counts.__ALL__ === 0) return result;
9320			for (var prop in counts) {
9321				if (counts.hasOwnProperty(prop)) {
9322					var count = counts[prop], isAll = prop === "__ALL__";
9323					if ( self.noAll && isAll ) continue;
9324					if (self.min <= 0 || count >= self.min) {
9325						if (tags.length > 0){
9326							index = _utils.inArray(prop, tags);
9327						} else {
9328							index++;
9329						}
9330						result.push({
9331							level: levelIndex,
9332							index: index,
9333							value: isAll ? "" : prop,
9334							text: isAll ? levelText : prop,
9335							count: count,
9336							percent: 1,
9337							size: self.largest,
9338							opacity: self.darkest
9339						});
9340						if (count < min) min = count;
9341						if (count > max) max = count;
9342					}
9343				}
9344			}
9345
9346			// if there's a limit set, remove other tags
9347			if (self.limit > 0 && result.length > self.limit) {
9348				result.sort(function (a, b) {
9349					return b.count - a.count;
9350				});
9351				result = result.slice(0, self.limit);
9352			}
9353
9354			// if adjustSize or adjustOpacity is enabled, calculate a percentage value used to calculate the appropriate font size and opacity
9355			if (!self.isMultiLevel && (self.adjustSize === true || self.adjustOpacity === true)) {
9356				var fontRange = self.largest - self.smallest;
9357				var opacityRange = self.darkest - self.lightest;
9358				for (var i = 0, l = result.length, tag; i < l; i++) {
9359					tag = result[i];
9360					tag.percent = (tag.count - min) / (max - min);
9361					tag.size = self.adjustSize ? Math.round((fontRange * tag.percent) + self.smallest) : self.largest;
9362					tag.opacity = self.adjustOpacity ? (opacityRange * tag.percent) + self.lightest : self.darkest;
9363				}
9364			}
9365
9366			// finally sort the tags using the sort options
9367			switch (self.sortBy){
9368				case "none":
9369					self.sort(result, "index", false);
9370					break;
9371				default:
9372					self.sort(result, self.sortBy, self.sortInvert);
9373					break;
9374			}
9375
9376			return result;
9377		},
9378		showControl: function(){
9379			return this.opt.search || !this.tags.every(function (tags) {
9380				return tags.length === 0;
9381			});
9382		},
9383		build: function (useAvailable) {
9384			var self = this, items = useAvailable ? self.tmpl.items.available() : self.tmpl.items.all();
9385			self.isMultiLevel = self.opt.tags.length > 0 && _is.object(self.opt.tags[0]);
9386			if (items.length > 0) {
9387				var tagObjects;
9388				if (self.isMultiLevel){
9389					$.each(self.opt.tags, function(i, level){
9390						tagObjects = self.createTagObjects(items, level.tags, i, level.all || self.il8n.all);
9391						if (!_is.empty(tagObjects)) self.tags.push(tagObjects);
9392					});
9393				} else {
9394					tagObjects = self.createTagObjects(items, self.opt.tags, 0, self.il8n.all);
9395					if (!_is.empty(tagObjects)) self.tags.push(tagObjects);
9396				}
9397			}
9398
9399			if (self.showControl() && _.filtering.hasCtrl(self.type)) {
9400				var pos = self.position, top, bottom;
9401				if (pos === "both" || pos === "top") {
9402					top = _.filtering.makeCtrl(self.type, self.tmpl, self, "top");
9403					if (top.create()) {
9404						top.append();
9405						self.ctrls.push(top);
9406					}
9407				}
9408				if (pos === "both" || pos === "bottom") {
9409					bottom = _.filtering.makeCtrl(self.type, self.tmpl, self, "bottom");
9410					if (bottom.create()) {
9411						bottom.append();
9412						self.ctrls.push(bottom);
9413					}
9414				}
9415			}
9416		},
9417		rebuild: function (useAvailable) {
9418			var self = this;
9419			self.tags.splice(0, self.tags.length);
9420			$.each(self.ctrls.splice(0, self.ctrls.length), function (i, control) {
9421				control.destroy();
9422			});
9423			self.build(useAvailable);
9424		},
9425		controls: function (tags, search) {
9426			var self = this;
9427			$.each(self.ctrls, function (i, control) {
9428				control.update(tags, search);
9429			});
9430		},
9431		hasAll: function(item, tags){
9432			return tags.every(function(arr){
9433				return arr.length === 0 || (_is.array(item.tags) && arr.every(function (tag) {
9434					return item.tags.indexOf(tag) !== -1;
9435				}));
9436			});
9437		},
9438		hasSome: function(item, tags){
9439			return tags.every(function(arr){
9440				return arr.length === 0 || (_is.array(item.tags) && arr.some(function (tag) {
9441					return item.tags.indexOf(tag) !== -1;
9442				}));
9443			});
9444		},
9445		isMatch: function(item, searchRegex){
9446			return _is.string(item.title) && searchRegex.test(item.title)
9447				|| _is.string(item.alt) && searchRegex.test(item.alt)
9448				|| _is.string(item.caption) && searchRegex.test(item.caption)
9449				|| _is.string(item.description) && searchRegex.test(item.description)
9450				|| _is.array(item.tags) && item.tags.some(function(tag){
9451					return searchRegex.test(tag);
9452				});
9453		},
9454		set: function (tags, search, updateState) {
9455			if (_is.string(tags)) tags = [[tags]];
9456			if (!_is.array(tags)) tags = [];
9457			var self = this, state;
9458			if (!self.arraysEqual(self.current, tags) || self.search !== search) {
9459				var prev = self.current.slice(), setFilter = function () {
9460					updateState = _is.boolean(updateState) ? updateState : true;
9461					if (updateState && !self.tmpl.state.exists()) {
9462						state = self.tmpl.state.get();
9463						self.tmpl.state.update(state, self.pushOrReplace);
9464					}
9465
9466					var searchChanged = self.search !== search,
9467						emptySearch = _is.empty(search);
9468
9469					if (_is.empty(tags) && emptySearch) {
9470						self.tmpl.items.reset();
9471					} else {
9472						var items = self.tmpl.items.all();
9473						if (!emptySearch){
9474							var regex = new RegExp(_str.escapeRegExp(search), "i");
9475							items = $.map(items, function(item){
9476								return self.isMatch(item, regex) ? item : null;
9477							});
9478						}
9479						if (self.mode === 'intersect') {
9480							items = $.map(items, function (item) {
9481								return self.hasAll(item, tags) ? item : null;
9482							});
9483						} else {
9484							items = $.map(items, function (item) {
9485								return self.hasSome(item, tags) ? item : null;
9486							});
9487						}
9488						self.tmpl.items.setAvailable(items);
9489					}
9490					self.current = tags.slice();
9491					self.search = search;
9492					if (searchChanged){
9493						self.rebuild(!emptySearch);
9494					} else {
9495						self.controls(self.current, self.search);
9496					}
9497					if (self.tmpl.pages) {
9498						self.tmpl.pages.rebuild();
9499						self.tmpl.pages.set(1, null, null, true);
9500					} else {
9501						self.tmpl.items.detach(self.tmpl.items.all());
9502						self.tmpl.items.create(self.tmpl.getAvailable(), true);
9503					}
9504
9505					if (updateState) {
9506						state = self.tmpl.state.get();
9507						self.tmpl.state.update(state, self.pushOrReplace);
9508					}
9509
9510					self.tmpl.trigger("after-filter-change", [self.current, prev]);
9511				};
9512				var e = self.tmpl.trigger("before-filter-change", [self.current, tags, setFilter]);
9513				if (e.isDefaultPrevented()) return false;
9514				setFilter();
9515				return true;
9516			}
9517			return false;
9518		},
9519		arraysEqual: function (arr1, arr2) {
9520			if (arr1.length !== arr2.length)
9521				return false;
9522
9523			arr1 = arr1.slice();
9524			arr2 = arr2.slice();
9525
9526			arr1.sort();
9527			arr2.sort();
9528			for (var i = arr1.length; i--;) {
9529				if (arr1[i] !== arr2[i])
9530					return false;
9531			}
9532			return true;
9533		},
9534		sort: function(tags, prop, invert){
9535			tags.sort(function(a, b){
9536
9537				if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop)){
9538					if (_is.string(a[prop]) && _is.string(b[prop])){
9539						var s1 = a[prop], s2 = b[prop];
9540						if (invert){
9541                            return s2.localeCompare(s1);
9542						} else {
9543                            return s1.localeCompare(s2);
9544						}
9545					} else {
9546						if (invert){
9547							return b[prop] - a[prop];
9548						}
9549						return a[prop] - b[prop];
9550					}
9551				}
9552				return 0;
9553
9554			});
9555		},
9556		apply: function (tags, search) {
9557			var self = this;
9558			self.set(tags, search, !self.tmpl.pages);
9559		}
9560	});
9561
9562	_.FilteringControl = _.Component.extend({
9563		construct: function (template, parent, position) {
9564			var self = this;
9565			self._super(template);
9566			self.filter = parent;
9567			self.position = position;
9568			self.$container = null;
9569		},
9570		create: function () {
9571			var self = this;
9572			self.$container = $("#" + self.tmpl.id + "_filtering-" + self.position);
9573			if (self.$container.length > 0){
9574				self._containerExisted = true;
9575				self.$container.removeClass(function(i, classNames){
9576					self._placeholderClasses = classNames.match(/(^|\s)fg-ph-\S+/g) || [];
9577					return self._placeholderClasses.join(' ');
9578				}).addClass([self.filter.cls.container, self.filter.theme].join(' '));
9579			} else {
9580				self.$container = $("<nav/>", {"class": [self.filter.cls.container, self.filter.theme].join(' ')});
9581			}
9582            self.$container.css("--fg-base-size", `${ self.filter.largest }px`).toggleClass('fg-multi-level', self.filter.isMultiLevel);
9583			return true;
9584		},
9585		destroy: function () {
9586			var self = this;
9587			if (self._containerExisted){
9588				self.$container.empty()
9589					.addClass(self._placeholderClasses.join(' '));
9590			} else {
9591				self.$container.remove();
9592			}
9593			self.$container = null;
9594		},
9595		append: function () {
9596			var self = this;
9597			if (self._containerExisted) return;
9598			if (self.position === "top") {
9599				self.$container.insertBefore(self.tmpl.$el);
9600			} else {
9601				self.$container.insertAfter(self.tmpl.$el);
9602			}
9603		},
9604		update: function (tags) {
9605		}
9606	});
9607
9608	_.filtering.register("default", _.Filtering, null, {
9609		type: "none",
9610		theme: null, // fg-light, fg-dark, fg-custom
9611		pushOrReplace: "push",
9612		position: "none",
9613		mode: "single",
9614		sortBy: "value", // "value", "count", "index", "none"
9615		sortInvert: false, // the direction of the sorting
9616		search: false,
9617		noAll: false,
9618		autoSelected: false,
9619		tags: [],
9620		min: 0,
9621		limit: 0,
9622		showCount: false,
9623		adjustSize: false,
9624		adjustOpacity: false,
9625		smallest: 12,
9626		largest: 16,
9627		lightest: 0.5,
9628		darkest: 1
9629	}, {
9630		container: "fg-filtering-container"
9631	}, null, -100);
9632
9633})(
9634	FooGallery.$,
9635	FooGallery,
9636	FooGallery.utils,
9637	FooGallery.utils.is,
9638	FooGallery.utils.str
9639);
9640(function($, _, _utils, _is){
9641
9642	_.Tags = _.Filtering.extend({
9643		construct: function( template ) {
9644			this._super( template );
9645			if ( ( this.hideTopTags = this.opt.search && this.position === "bottom" ) ) {
9646				this.position = "both";
9647			}
9648		}
9649	});
9650
9651	_.TagsControl = _.FilteringControl.extend({
9652		construct: function(template, parent, position){
9653			this._super(template, parent, position);
9654			this.$container = null;
9655            this.$wrap = null;
9656			this.searchEnabled = this.position === "top" && this.filter.opt.search;
9657			this.search = {
9658				$wrap: null,
9659				$inner: null,
9660				$input: null,
9661				$clear: null,
9662				$submit: null
9663			};
9664			this.lists = [];
9665            this.dropdownEnabled = this.isDropdownStyle();
9666            this.collapseEnabled = this.filter.opt.collapse;
9667            this.collapse = null;
9668            this.onCollapseChange = this.onCollapseChange.bind( this );
9669            this._wrappedAt = 0;
9670            this.onLayout = this.onLayout.bind( this );
9671		},
9672        isDropdownStyle: function() {
9673            return ['dropdown','dropdown-block'].includes(this?.filter?.opt?.style);
9674        },
9675        shouldRenderTags: function() {
9676            return ( this.position === "bottom" || (this.position === "top" && !this.filter.hideTopTags ) )
9677                && this.filter.tags.length > 0;
9678        },
9679		create: function(){
9680            if (this._super()) {
9681                const {
9682                    filter: {
9683                        cls,
9684                        opt,
9685                        search,
9686                        tags,
9687                        isMultiLevel,
9688                        showCount
9689                    }
9690                } = this;
9691                if (this.searchEnabled){
9692                    this.$container.append(this.createSearch(search));
9693                    if (!_is.empty(opt.searchPosition)){
9694                        this.$container.addClass("fg-search-" + opt.searchPosition);
9695                    }
9696                }
9697                this.$container.addClass("fg-tags-" + opt.align);
9698                if ( _is.string( opt.style ) ) {
9699                    this.$container.addClass("fg-style-" + opt.style);
9700                }
9701
9702                if (this.shouldRenderTags()){
9703                    if (this.collapseEnabled) {
9704                        if (globalThis.FooGallery_autoMobileBreakpoint === false) {
9705                            // The editor controls mobile mode without resizing the browser.
9706                            this.dropdownEnabled = this.isDropdownStyle() || _.isMobile;
9707                        } else {
9708                            this.collapse = matchMedia(`(max-width: 600px)`);
9709                            this.collapse.addEventListener('change', this.onCollapseChange);
9710                            this.dropdownEnabled = this.isDropdownStyle() || this.collapse.matches;
9711                        }
9712                    }
9713                    this.$wrap = this.createWrap().appendTo(this.$container);
9714                    let $wrap = this.$wrap;
9715                    if ( this.dropdownEnabled ) {
9716                        $wrap = $( "<div/>", { "class": cls.dropdown.wrap } ).appendTo( this.$wrap );
9717                    }
9718                    tags.forEach( (tagsGroup, i) => this.lists.push( this.createList( tagsGroup, i ).appendTo( $wrap ) ) );
9719                    if (!isMultiLevel && showCount === true) {
9720                        this.$container.addClass(cls.showCount);
9721                    }
9722                    this.tmpl.on("layout", this.onLayout);
9723                } else {
9724                    this.$container.addClass(cls.noTags);
9725                }
9726                return true;
9727            }
9728            return false;
9729		},
9730		createSearch: function(search){
9731			var self = this, cls = self.filter.cls.search, il8n = self.filter.il8n;
9732
9733			self.search.$wrap = $("<div/>", {"class": cls.wrap});
9734
9735			self.search.$inner = $("<div/>", {"class": cls.inner}).appendTo(self.search.$wrap);
9736
9737			self.search.$input = $("<input/>", {"type": "text", "class": cls.input, "placeholder": il8n.searchPlaceholder, "name":`${self.tmpl.id}__search`})
9738				.on("input.foogallery", {self: self}, self.onSearchInput)
9739				.on("keydown.foogallery", {self: self}, self.onSearchKeydown)
9740				.appendTo(self.search.$inner);
9741
9742			self.search.$clear = $("<button/>", {"type": "button","class": cls.clear})
9743				.append($("<span/>", {"class": cls.reader, text: il8n.searchClear}))
9744				.append(_.icons.get("close"))
9745				.on("click.foogallery", {self: self}, self.onSearchClear)
9746				.appendTo(self.search.$inner);
9747
9748			self.search.$submit = $("<button/>", {"type": "button","class": cls.submit})
9749				.append($("<span/>", {"class": cls.reader, text: il8n.searchSubmit}))
9750				.append(_.icons.get("search"))
9751				.on("click.foogallery", {self: self}, self.onSearchSubmit)
9752				.appendTo(self.search.$inner);
9753
9754			if (!_is.empty(search)){
9755				self.search.$wrap.addClass(cls.hasValue);
9756				self.search.$input.val(search).attr("placeholder", search);
9757			}
9758
9759			return self.search.$wrap;
9760		},
9761        createWrap: function(){
9762            return $("<div/>", {"class": this.filter.cls.wrap});
9763        },
9764        createList: function( tags, level ) {
9765            const self = this;
9766            let $list, $appendItems;
9767            let cls = self.filter.cls;
9768            const selected = ( tag, i ) => i === 0 && ( self.filter.autoSelected || _is.empty( tag?.value ) );
9769            if ( self.dropdownEnabled ) {
9770                $list = $( "<label/>", { "class": cls.dropdown.item } ).append(
9771                    $( "<span/>" ).addClass( cls.dropdown.icon ).append( _.icons.get( 'arrow-down' ) )
9772                );
9773                $appendItems = $( "<select/>", {
9774                    "class": cls.dropdown.select,
9775                    "name": `${ self.tmpl.id }__tags-${ level }`
9776                } )
9777                    .on( 'change.foogallery', { self }, self.onDropdownChange )
9778                    .prependTo( $list );
9779            } else {
9780                $list = $appendItems = $( "<ul/>", { "class": cls.list } );
9781            }
9782            tags.forEach( ( tag, i ) => $appendItems.append( self.createItem( tag, selected( tag, i ) ) ) );
9783
9784            return $list;
9785        },
9786        onCollapseChange: function() {
9787            const prev = this.dropdownEnabled;
9788            this.dropdownEnabled = this.isDropdownStyle() || this.collapse.matches;
9789            if ( this.dropdownEnabled !== prev ) {
9790                this.recreateLists( prev );
9791            }
9792        },
9793        destroyLists: function( wasDropdown ) {
9794            const { sel } = this.filter;
9795            this.lists.forEach( $list => {
9796                if ( wasDropdown ) {
9797                    $list.find(sel.dropdown.select).off("change.foogallery", this.onDropdownChange);
9798                } else {
9799                    $list.find(sel.link).off("click.foogallery", this.onLinkClick);
9800                }
9801                $list.remove();
9802            } );
9803            this.$wrap?.empty();
9804            this.lists = [];
9805        },
9806        recreateLists: function( wasDropdown ) {
9807            const { cls } = this.filter;
9808            this.destroyLists( wasDropdown );
9809            let $wrap = this.$wrap;
9810            if ( this.dropdownEnabled ) {
9811                $wrap = $( "<div/>", { "class": cls.dropdown.wrap } ).appendTo( this.$wrap );
9812            }
9813            this.filter.tags.forEach( (tagsGroup, i) => this.lists.push( this.createList( tagsGroup, i ).appendTo( $wrap ) ) );
9814            this.update(this.filter.current, this.filter.search);
9815        },
9816		destroy: function(){
9817            if (this.collapse) {
9818                this.collapse.removeEventListener('change', this.onCollapseChange);
9819                this.collapse = null;
9820            }
9821            this.destroyLists( this.dropdownEnabled );
9822            this.tmpl.off("layout", this.onLayout);
9823            this._super();
9824		},
9825		update: function(tags, search){
9826			var self = this, cls = self.filter.cls, sel = self.filter.sel;
9827			if (self.searchEnabled){
9828				self.search.$wrap.toggleClass(cls.search.hasValue, !_is.empty(search));
9829				self.search.$input.val(search);
9830			}
9831			self.lists.forEach(function($list, i){
9832                if (self.dropdownEnabled) {
9833                    if ( !_is.empty( tags[i] ) ) {
9834                        $list.find(sel.dropdown.select).val( tags[i].length === 1 ? tags[i][0] : tags[i] );
9835                    } else {
9836                        $list.find(sel.dropdown.select).val('');
9837                    }
9838                } else {
9839                    $list.find(sel.item).removeClass(cls.selected).each(function(){
9840                        var $item = $(this), tag = $item.data("tag") + ""; // force string value
9841                        var isAll = _is.empty(tag);
9842                        var isSelected = (isAll && _is.empty(tags[i])) || (!isAll && _utils.inArray(tag, tags[i]) !== -1);
9843                        $item.toggleClass(cls.selected, isSelected);
9844                    });
9845                }
9846			});
9847		},
9848		createItem: function( tag, selected ){
9849            const {
9850                dropdownEnabled,
9851                filter: {
9852                    cls,
9853                    isMultiLevel,
9854                    showCount
9855                }
9856            } = this;
9857            if ( dropdownEnabled ) {
9858                const $option = $( "<option/>", {
9859                    value: tag.value,
9860                    text: _.decodeHTMLEntities( _is.string(tag.text) ? tag.text : tag.value )
9861                } ).data( 'tagObject', tag );
9862
9863                if ( selected ) {
9864                    $option.attr( 'selected', '' );
9865                }
9866                return $option;
9867            } else {
9868                const $li = $("<li/>", {"class": cls.item}).attr("data-tag", tag.value),
9869                    $span = $("<span/>", {"class": cls.text}).html(_is.string(tag.text) ? tag.text : tag.value),
9870                    $link = $("<a/>", {"href": "#tag-" + tag.value, "class": cls.link})
9871                        .on("click.foogallery", {self: this, tag: tag}, this.onLinkClick)
9872                        .css("font-size", tag.size)
9873                        .css("opacity", tag.opacity)
9874                        .append($span)
9875                        .appendTo($li);
9876
9877                if ( selected ) {
9878                    $li.addClass( cls.selected );
9879                }
9880                if (!isMultiLevel && showCount === true){
9881                    $link.append($("<span/>", {"text": tag.count, "class": cls.count}));
9882                }
9883                return $li;
9884            }
9885		},
9886        toggleTag: function( tag ) {
9887            if ( !_is.object( tag ) ) {
9888                return;
9889            }
9890            const self = this;
9891            const { current, mode, search } = self.filter;
9892            // get the currently applied filter tags
9893            let tags = current.map( obj => _is.array( obj ) ? obj.slice() : obj );
9894            if ( !_is.empty( tag.value ) ) {
9895                if ( !_is.array( tags[ tag.level ] ) ) {
9896                    tags[ tag.level ] = [];
9897                }
9898                let i = _utils.inArray( tag.value, tags[ tag.level ] );
9899                switch ( mode ) {
9900                    case "union":
9901                    case "intersect":
9902                        if ( i === -1 ) {
9903                            tags[ tag.level ].push( tag.value );
9904                        } else {
9905                            tags[ tag.level ].splice( i, 1 );
9906                        }
9907                        break;
9908                    case "single":
9909                    default:
9910                        if ( i === -1 ) {
9911                            tags[ tag.level ] = [ tag.value ];
9912                        } else {
9913                            tags[ tag.level ] = [];
9914                        }
9915                        break;
9916                }
9917            } else {
9918                tags[ tag.level ] = [];
9919            }
9920            if ( tags.every( _is.empty ) ) {
9921                tags = [];
9922            }
9923            self.filter.apply( tags, search );
9924        },
9925        onDropdownChange: function(e) {
9926            let selected = e.target.querySelector(`[value="${ e.target.value }"]`);
9927            e.data.self.toggleTag($(selected).data('tagObject'));
9928        },
9929		onLinkClick: function(e){
9930			e.preventDefault();
9931            e.data.self.toggleTag(e.data.tag);
9932		},
9933        onLayout: function( e, width ){
9934            const self = this;
9935            if (self.dropdownEnabled) {
9936                self.lists.forEach( $list => $list.removeClass( 'fg-wrapped' ) );
9937                self.$container.removeClass( 'fg-has-wrapped' );
9938                self._wrappedAt = 0;
9939            } else {
9940                let hasWrapped = false;
9941                self.lists.forEach( $list => {
9942                    const list = $list.get(0);
9943                    const wrapped = list?.lastElementChild?.offsetTop !== list?.firstElementChild?.offsetTop;
9944                    $list.toggleClass( 'fg-wrapped', wrapped );
9945                    if ( !hasWrapped && wrapped ) hasWrapped = true;
9946                } );
9947                if ( width >= self._wrappedAt ) {
9948                    self.$container.toggleClass( 'fg-has-wrapped', hasWrapped );
9949                    self._wrappedAt = hasWrapped ? width : 0;
9950                }
9951            }
9952        },
9953		onSearchInput: function(e){
9954			var self = e.data.self, cls = self.filter.cls.search;
9955			var hasValue = !_is.empty(self.search.$input.val()) || self.search.$input.attr("placeholder") !== self.filter.il8n.searchPlaceholder;
9956			self.search.$wrap.toggleClass(cls.hasValue, hasValue);
9957		},
9958		onSearchKeydown: function(e){
9959			if (e.which === 13){
9960				var self = e.data.self;
9961				self.filter.apply([], self.search.$input.val());
9962			}
9963		},
9964		onSearchClear: function(e){
9965			e.preventDefault();
9966			var self = e.data.self;
9967			self.search.$wrap.removeClass(self.filter.cls.search.hasValue);
9968			self.search.$input.val('');
9969			if (self.search.$input.attr("placeholder") !== self.filter.il8n.searchPlaceholder){
9970				self.filter.apply([], '');
9971			}
9972		},
9973		onSearchSubmit: function(e){
9974			e.preventDefault();
9975			var self = e.data.self;
9976			self.filter.apply([], self.search.$input.val());
9977		}
9978	});
9979
9980	_.filtering.register("tags", _.Tags, _.TagsControl, {
9981		type: "tags",
9982		position: "top",
9983        style: null, // button, button-block, pill, pill-block, dropdown, dropdown-block
9984        align: "center",
9985		pushOrReplace: "push",
9986		searchPosition: "above-center",
9987        collapse: false
9988	}, {
9989		showCount: "fg-show-count",
9990		noTags: "fg-no-tags",
9991        wrap: "fg-tag-wrap",
9992		list: "fg-tag-list",
9993		item: "fg-tag-item",
9994		link: "fg-tag-link",
9995		text: "fg-tag-text",
9996		count: "fg-tag-count",
9997		selected: "fg-selected",
9998        wrapped: "fg-wrapped",
9999		search: {
10000			wrap: "fg-search-wrap",
10001			inner: "fg-search-inner",
10002			input: "fg-search-input",
10003			clear: "fg-search-clear",
10004			submit: "fg-search-submit",
10005			hasValue: "fg-search-has-value",
10006			reader: "fg-sr-only"
10007		},
10008        dropdown: {
10009            wrap: "fg-tag-dropdown-wrap",
10010            list: "fg-tag-dropdown-list",
10011            item: "fg-tag-dropdown",
10012            select: "fg-tag-dropdown-select",
10013            icon: "fg-tag-dropdown-icon"
10014        }
10015	}, {
10016		all: "All",
10017		none: "No items found.",
10018		searchPlaceholder: "Search gallery...",
10019		searchSubmit: "Submit search",
10020		searchClear: "Clear search"
10021	}, -100);
10022
10023})(
10024		FooGallery.$,
10025		FooGallery,
10026		FooGallery.utils,
10027		FooGallery.utils.is
10028);
10029
10030(function(_, _is, _fn, _obj){
10031
10032	_.PagingFactory = _.Factory.extend(/** @lends FooGallery.PagingFactory */{
10033		/**
10034		 * @summary A factory for paging types allowing them to be easily registered and created.
10035		 * @memberof FooGallery
10036		 * @constructs PagingFactory
10037		 * @description The plugin makes use of an instance of this class exposed as {@link FooGallery.paging}.
10038		 * @augments FooGallery.Factory
10039		 * @borrows FooGallery.utils.Class.extend as extend
10040		 * @borrows FooGallery.utils.Class.override as override
10041		 */
10042		construct: function(){
10043			/**
10044			 * @summary An object containing all registered paging types.
10045			 * @memberof FooGallery.PagingFactory#
10046			 * @name registered
10047			 * @type {Object.<string, Object>}
10048			 * @readonly
10049			 * @example {@caption The following shows the structure of this object. The `<name>` placeholders would be the name the class was registered with.}
10050			 * {
10051			 * 	"<name>": {
10052			 * 		"name": <string>,
10053			 * 		"klass": <function>,
10054			 * 		"ctrl": <function>,
10055			 * 		"priority": <number>
10056			 * 	},
10057			 * 	"<name>": {
10058			 * 		"name": <string>,
10059			 * 		"klass": <function>,
10060			 * 		"ctrl": <function>,
10061			 * 		"priority": <number>
10062			 * 	},
10063			 * 	...
10064			 * }
10065			 */
10066			this.registered = {};
10067		},
10068		/**
10069		 * @summary Registers a paging `type` constructor with the factory using the given `name` and `test` function.
10070		 * @memberof FooGallery.PagingFactory#
10071		 * @function register
10072		 * @param {string} name - The friendly name of the class.
10073		 * @param {FooGallery.Paging} type - The paging type constructor to register.
10074		 * @param {FooGallery.PagingControl} [ctrl] - An optional control to register for the paging type.
10075		 * @param {object} [options={}] - The default options for the paging type.
10076		 * @param {object} [classes={}] - The CSS classes for the paging type.
10077		 * @param {object} [il8n={}] - The il8n strings for the paging type.
10078		 * @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.
10079		 * @returns {boolean} `true` if the `klass` was successfully registered.
10080		 */
10081		register: function(name, type, ctrl, options, classes, il8n, priority){
10082			var self = this, result = self._super(name, type, priority);
10083			if (result){
10084				var reg = self.registered;
10085				reg[name].ctrl = _is.fn(ctrl) ? ctrl : null;
10086				reg[name].opt = _is.hash(options) ? options : {};
10087				reg[name].cls = _is.hash(classes) ? classes : {};
10088				reg[name].il8n = _is.hash(il8n) ? il8n : {};
10089			}
10090			return result;
10091		},
10092		type: function(options){
10093			var self = this, opt;
10094			return _is.hash(options) && _is.hash(opt = options.paging) && _is.string(opt.type) && self.contains(opt.type) ? opt.type : null;
10095		},
10096		merge: function(options){
10097			options = _obj.extend({}, options);
10098			var self = this, type = self.type(options),
10099					reg = self.registered,
10100					def = reg["default"].opt,
10101					def_cls = reg["default"].cls,
10102					def_il8n = reg["default"].il8n,
10103					opt = _is.hash(options.paging) ? options.paging : {},
10104					cls = _is.hash(options.cls) && _is.hash(options.cls.paging) ? _obj.extend({}, options.cls.paging) : {},
10105					il8n = _is.hash(options.il8n) && _is.hash(options.il8n.paging) ? _obj.extend({}, options.il8n.paging) : {};
10106
10107			if (!_is.hash(options.cls)) options.cls = {};
10108			if (!_is.hash(options.il8n)) options.il8n = {};
10109			if (type !== "default" && self.contains(type)){
10110				options.paging = _obj.extend({}, def, reg[type].opt, opt, {type: type});
10111				options.cls = _obj.extend(options.cls, {paging: def_cls}, {paging: reg[type].cls}, {paging: cls});
10112				options.il8n = _obj.extend(options.il8n, {paging: def_il8n}, {paging: reg[type].il8n}, {paging: il8n});
10113			} else {
10114				options.paging = _obj.extend({}, def, opt, {type: type});
10115				options.cls = _obj.extend(options.cls, {paging: def_cls}, {paging: cls});
10116				options.il8n = _obj.extend(options.il8n, {paging: def_il8n}, {paging: il8n});
10117			}
10118			return options;
10119		},
10120		/**
10121		 * @summary Checks if the factory contains a control registered using the supplied `name`.
10122		 * @memberof FooGallery.PagingFactory#
10123		 * @function hasCtrl
10124		 * @param {string} name - The friendly name of the class.
10125		 * @returns {boolean}
10126		 */
10127		hasCtrl: function(name){
10128			var self = this, reg = self.registered[name];
10129			return _is.hash(reg) && _is.fn(reg.ctrl);
10130		},
10131		/**
10132		 * @summary Create a new instance of a control class registered with the supplied `name` and arguments.
10133		 * @memberof FooGallery.PagingFactory#
10134		 * @function makeCtrl
10135		 * @param {string} name - The friendly name of the class.
10136		 * @param {FooGallery.Template} template - The template creating the control.
10137		 * @param {FooGallery.Paging} parent - The parent paging class creating the control.
10138		 * @param {string} position - The position the control will be displayed at.
10139		 * @returns {?FooGallery.PagingControl}
10140		 */
10141		makeCtrl: function(name, template, parent, position){
10142			var self = this, reg = self.registered[name];
10143			if (_is.hash(reg) && _is.fn(reg.ctrl)){
10144				return new reg.ctrl(template, parent, position);
10145			}
10146			return null;
10147		}
10148	});
10149
10150	/**
10151	 * @summary The factory used to register and create the various paging types of FooGallery.
10152	 * @memberof FooGallery
10153	 * @name paging
10154	 * @type {FooGallery.PagingFactory}
10155	 */
10156	_.paging = new _.PagingFactory();
10157
10158})(
10159		FooGallery,
10160		FooGallery.utils.is,
10161		FooGallery.utils.fn,
10162		FooGallery.utils.obj
10163);
10164(function ($, _, _utils, _is) {
10165
10166	_.Paging = _.Component.extend({
10167		construct: function (template) {
10168			var self = this;
10169			/**
10170			 * @ignore
10171			 * @memberof FooGallery.Paging#
10172			 * @function _super
10173			 */
10174			self._super(template);
10175			self.opt = self.tmpl.opt.paging;
10176			self.cls = self.tmpl.cls.paging;
10177			self.il8n = self.tmpl.il8n.paging;
10178			self.sel = self.tmpl.sel.paging;
10179			self.pushOrReplace = self.opt.pushOrReplace;
10180			self.type = self.opt.type;
10181			self.theme = self.opt.theme ?? template.getCSSClass('theme');
10182			self.size = self.opt.size;
10183			self.position = self.opt.position;
10184			self.scrollToTop = self.opt.scrollToTop;
10185			self.current = 0;
10186			self.total = 0;
10187			self.ctrls = [];
10188			self._pages = [];
10189		},
10190		init: function(){},
10191		fromHash: function(hash){
10192			var parsed = parseInt(hash);
10193			return isNaN(parsed) ? null : parsed;
10194		},
10195		toHash: function(value){
10196			return _is.number(value) && value > 0 ? value.toString() : null;
10197		},
10198		getState: function(){
10199			return this.isValid(this.current) ? this.current : null;
10200		},
10201		setState: function(state){
10202			this.rebuild();
10203			var shouldScroll = false;
10204			if (!!state.item && !this.contains(state.page, state.item)){
10205				state.page = this.find(state.item);
10206				state.page = state.page !== 0 ? state.page : 1;
10207				shouldScroll = true;
10208			}
10209			this.set(state.page, shouldScroll, false, false);
10210		},
10211		destroy: function () {
10212			var self = this;
10213			self._pages.splice(0, self._pages.length);
10214			$.each(self.ctrls.splice(0, self.ctrls.length), function (i, control) {
10215				control.destroy();
10216			});
10217			self._super();
10218		},
10219		build: function () {
10220			var self = this, items = self.tmpl.items.available(),
10221				size = self.size > 0 ? self.size : items.length;
10222			// Keep an unlimited page sized to the current matches on every rebuild.
10223			self.total = self.size > 0 && items.length > 0 ? Math.ceil(items.length / self.size) : 1;
10224			for (var i = 0; i < self.total; i++) {
10225				self._pages.push(items.splice(0, size));
10226			}
10227			if (self.total > 1 && _.paging.hasCtrl(self.type)) {
10228				var pos = self.position, top, bottom;
10229				if (pos === "both" || pos === "top") {
10230					top = _.paging.makeCtrl(self.type, self.tmpl, self, "top");
10231					if (top.create()) {
10232						top.append();
10233						self.ctrls.push(top);
10234					}
10235				}
10236				if (pos === "both" || pos === "bottom") {
10237					bottom = _.paging.makeCtrl(self.type, self.tmpl, self, "bottom");
10238					if (bottom.create()) {
10239						bottom.append();
10240						self.ctrls.push(bottom);
10241					}
10242				}
10243			}
10244		},
10245		rebuild: function () {
10246			var self = this;
10247			self.current = 0;
10248			self.total = 0;
10249			self._pages.splice(0, self._pages.length);
10250			$.each(self.ctrls.splice(0, self.ctrls.length), function (i, control) {
10251				control.destroy();
10252			});
10253			self.build();
10254		},
10255		all: function () {
10256			return this._pages.slice();
10257		},
10258		available: function () {
10259			return this.get(this.current);
10260		},
10261		controls: function (pageNumber) {
10262			var self = this;
10263			if (self.isValid(pageNumber)) {
10264				$.each(self.ctrls, function (i, control) {
10265					control.update(pageNumber);
10266				});
10267			}
10268		},
10269		isValid: function (pageNumber) {
10270			return _is.number(pageNumber) && pageNumber > 0 && pageNumber <= this.total;
10271		},
10272		number: function (value) {
10273			return this.isValid(value) ? value : (this.current === 0 ? 1 : this.current);
10274		},
10275		create: function (pageNumber, isFilter) {
10276			var self = this;
10277			pageNumber = self.number(pageNumber);
10278
10279			var pageIndex = pageNumber - 1, pageItems = self._pages[pageIndex], detach;
10280			if (isFilter){
10281				detach = self.tmpl.items.all();
10282			} else {
10283				detach = self._pages.reduce(function(detach, page, index){
10284					return index === pageIndex ? detach : detach.concat(page);
10285				}, self.tmpl.items.unavailable());
10286			}
10287
10288			self.current = pageNumber;
10289			self.tmpl.items.detach(detach);
10290			self.tmpl.items.create(pageItems, true);
10291		},
10292		get: function (pageNumber) {
10293			var self = this;
10294			if (self.isValid(pageNumber)) {
10295				pageNumber = self.number(pageNumber);
10296				return self._pages[pageNumber - 1];
10297			}
10298			return [];
10299		},
10300		set: function (pageNumber, scroll, updateState, isFilter) {
10301			var self = this;
10302			if (self.isValid(pageNumber)) {
10303				self.controls(pageNumber);
10304				var num = self.number(pageNumber), state;
10305				if (num !== self.current) {
10306					var prev = self.current, setPage = function () {
10307						updateState = _is.boolean(updateState) ? updateState : true;
10308						isFilter = _is.boolean(isFilter) ? isFilter : false;
10309						if (updateState && self.current === 1 && !self.tmpl.state.exists()) {
10310							state = self.tmpl.state.get();
10311							self.tmpl.state.update(state, self.pushOrReplace);
10312						}
10313						self.create(num, isFilter);
10314						if (updateState) {
10315							state = self.tmpl.state.get();
10316							self.tmpl.state.update(state, self.pushOrReplace);
10317						}
10318						self.tmpl.trigger("page-change", [self.current, prev, isFilter]);
10319						if (self.scrollToTop && _is.boolean(scroll) ? scroll : false) {
10320							var page = self.get(self.current);
10321							if (page.length > 0) {
10322								page[0].scrollTo("top");
10323							}
10324						}
10325						self.tmpl.trigger("after-page-change", [self.current, prev, isFilter]);
10326					};
10327					var e = self.tmpl.trigger("before-page-change", [self.current, num, setPage, isFilter]);
10328					if (e.isDefaultPrevented()) return false;
10329					setPage();
10330					return true;
10331				}
10332			}
10333			return false;
10334		},
10335		find: function (item) {
10336			var self = this;
10337			for (var i = 0, l = self._pages.length; i < l; i++) {
10338				if (_utils.inArray(item, self._pages[i]) !== -1) {
10339					return i + 1;
10340				}
10341			}
10342			return 0;
10343		},
10344		contains: function (pageNumber, item) {
10345			var items = this.get(pageNumber);
10346			return _utils.inArray(item, items) !== -1;
10347		},
10348		first: function () {
10349			this.goto(1);
10350		},
10351		last: function () {
10352			this.goto(this._pages.length);
10353		},
10354		prev: function () {
10355			this.goto(this.current - 1);
10356		},
10357		next: function () {
10358			this.goto(this.current + 1);
10359		},
10360		goto: function (pageNumber) {
10361			this.set(pageNumber, true);
10362		}
10363	});
10364
10365	_.PagingControl = _.Component.extend({
10366		construct: function (template, parent, position) {
10367			var self = this;
10368			self._super(template);
10369			self.pages = parent;
10370			self.position = position;
10371			self.$container = null;
10372			self._containerExisted = false;
10373			self._placeholderClasses = [];
10374		},
10375		create: function () {
10376			var self = this;
10377			self.$container = $("#" + self.tmpl.id + "_paging-" + self.position);
10378			if (self.$container.length > 0){
10379				self._containerExisted = true;
10380				self.$container.removeClass(function(i, classNames){
10381					self._placeholderClasses = classNames.match(/(^|\s)fg-ph-\S+/g) || [];
10382					return self._placeholderClasses.join(' ');
10383				}).addClass([self.pages.cls.container, self.pages.theme].join(' '));
10384			} else {
10385				self.$container = $("<nav/>", {"class": [self.pages.cls.container, self.pages.theme].join(' ')});
10386			}
10387			return true;
10388		},
10389		destroy: function () {
10390			var self = this;
10391			if (self._containerExisted){
10392				self.$container.empty()
10393					.removeClass()
10394					.addClass(self._placeholderClasses.join(' '));
10395			} else {
10396				self.$container.remove();
10397			}
10398			self.$container = null;
10399		},
10400		append: function () {
10401			var self = this;
10402			if (self._containerExisted) return;
10403			if (self.position === "top") {
10404				self.$container.insertBefore(self.tmpl.$el);
10405			} else {
10406				self.$container.insertAfter(self.tmpl.$el);
10407			}
10408		},
10409		update: function (pageNumber) {
10410		}
10411	});
10412
10413	_.paging.register("default", _.Paging, null, {
10414		type: "none",
10415		theme: null,
10416		size: 30,
10417		pushOrReplace: "push",
10418		position: "none",
10419		scrollToTop: true
10420	}, {
10421		container: "fg-paging-container"
10422	}, null, -100);
10423
10424})(
10425		FooGallery.$,
10426		FooGallery,
10427		FooGallery.utils,
10428		FooGallery.utils.is
10429);
10430
10431(function($, _, _utils, _is, _fn){
10432
10433	_.Infinite = _.Paging.extend({
10434		construct: function(template){
10435			var self = this;
10436			self._super(template);
10437			self.distance = self.opt.distance;
10438			self._created = [];
10439		},
10440		init: function(){
10441			var self = this;
10442			self.checkBounds();
10443			self.tmpl.on("ready.infinite", self.checkReadyBounds, self);
10444			self.tmpl.$scrollParent.on("scroll" + self.tmpl.namespace, {self: self}, _fn.throttle(function () {
10445				if (!self.tmpl.destroying && !self.tmpl.destroyed){
10446					self.checkBounds();
10447				}
10448			}, 50));
10449		},
10450		destroy: function(){
10451			var self = this;
10452			self.tmpl.off("ready.infinite", self.checkReadyBounds, self);
10453			self.tmpl.$scrollParent.off(self.tmpl.namespace);
10454		},
10455		checkReadyBounds: function(){
10456			var self = this;
10457			if (self.tmpl.$scrollParent.scrollTop() > 0){
10458				self.checkBounds();
10459			}
10460		},
10461		checkBounds: function(){
10462			var self = this, page = self.get(self.current), bounds;
10463			if (!self.tmpl.initializing && !_is.empty(page) && self._created.length < self.total){
10464				bounds = self.tmpl.el.getBoundingClientRect();
10465				if (bounds !== null && bounds.bottom - window.innerHeight < self.distance){
10466					self.set(self.current + 1, false, true, false);
10467					self.checkBounds();
10468				}
10469			}
10470		},
10471		build: function(){
10472			var self = this;
10473			self._super();
10474			self._created = [];
10475		},
10476		available: function(){
10477			var self = this, items = [];
10478			for (var i = 0, l = self._created.length, num, page; i < l; i++){
10479				num = i + 1;
10480				page = self.get(num);
10481				if (!_is.empty(page)){
10482					items.push.apply(items, page);
10483				}
10484			}
10485			return items;
10486		},
10487		create: function(pageNumber, isFilter){
10488			var self = this;
10489			pageNumber = self.number(pageNumber);
10490			var create = [], detach;
10491			if (isFilter){
10492				detach = self.tmpl.items.all();
10493			} else {
10494				detach = self._pages.reduce(function(detach, page, index){
10495					return index < pageNumber ? detach : detach.concat(page);
10496				}, self.tmpl.items.unavailable());
10497			}
10498
10499			for (var i = 0; i < pageNumber; i++){
10500				if (_utils.inArray(i, self._created) === -1){
10501					create.push.apply(create, self._pages[i]);
10502					self._created.push(i);
10503				}
10504			}
10505			self.current = pageNumber;
10506			self.tmpl.items.detach(detach);
10507			self.tmpl.items.create(create, true);
10508		}
10509	});
10510
10511	_.paging.register("infinite", _.Infinite, null, {
10512		type: "infinite",
10513		pushOrReplace: "replace",
10514		distance: 200
10515	});
10516
10517
10518})(
10519	FooGallery.$,
10520	FooGallery,
10521	FooGallery.utils,
10522	FooGallery.utils.is,
10523	FooGallery.utils.fn
10524);
10525
10526(function($, _, _utils, _is){
10527
10528	_.LoadMore = _.Paging.extend({
10529		construct: function(template){
10530			var self = this;
10531			self._super(template);
10532			self._created = [];
10533		},
10534		build: function(){
10535			var self = this;
10536			self._super();
10537			self._created = [];
10538		},
10539		create: function(pageNumber, isFilter){
10540			var self = this;
10541			pageNumber = self.number(pageNumber);
10542			var create = [], detach;
10543			if (isFilter){
10544				detach = self.tmpl.items.all();
10545			} else {
10546				detach = self._pages.reduce(function(detach, page, index){
10547					return index < pageNumber ? detach : detach.concat(page);
10548				}, self.tmpl.items.unavailable());
10549			}
10550
10551			for (var i = 0; i < pageNumber; i++){
10552				if (_utils.inArray(i, self._created) === -1){
10553					create.push.apply(create, self._pages[i]);
10554					self._created.push(i);
10555				}
10556			}
10557			self.current = pageNumber;
10558			self.tmpl.items.detach(detach);
10559			self.tmpl.items.create(create, true);
10560		},
10561		available: function(){
10562			var self = this, items = [];
10563			for (var i = 0, l = self._created.length, num, page; i < l; i++){
10564				num = i + 1;
10565				page = self.get(num);
10566				if (!_is.empty(page)){
10567					items.push.apply(items, page);
10568				}
10569			}
10570			return items;
10571		},
10572		loadMore: function(){
10573			var self = this, page = self.get(self.current);
10574			if (!_is.empty(page) && self._created.length < self.total){
10575				self.set(self.current + 1, false, true, false);
10576			}
10577			if (self._created.length >= self.total){
10578				if (!_is.empty(self.ctrls)){
10579					$.each(self.ctrls.splice(0, self.ctrls.length), function(i, control){
10580						control.destroy();
10581					});
10582				}
10583			}
10584		}
10585	});
10586
10587	_.LoadMoreControl = _.PagingControl.extend({
10588		construct: function(template, parent, position){
10589			this._super(template, parent, position);
10590			this.$button = null;
10591		},
10592		create: function(){
10593			var self = this;
10594			if (self._super()){
10595				self.$button = $("<button/>", {"class": self.pages.cls.button, "type": "button"}).html(self.pages.il8n.button)
10596					.on("click.foogallery", {self: self}, self.onButtonClick)
10597					.appendTo(self.$container);
10598				return true;
10599			}
10600			return false;
10601		},
10602		destroy: function(){
10603			var self = this;
10604			self.$button.off("click.foogallery", self.onButtonClick);
10605			self.$button = null;
10606			self._super();
10607		},
10608		onButtonClick: function(e){
10609			e.preventDefault();
10610			e.data.self.pages.loadMore();
10611		}
10612	});
10613
10614	_.paging.register("loadMore", _.LoadMore, _.LoadMoreControl, {
10615		type: "loadMore",
10616		position: "bottom",
10617		pushOrReplace: "replace"
10618	}, {
10619		button: "fg-load-more"
10620	}, {
10621		button: "Load More"
10622	});
10623
10624
10625})(
10626	FooGallery.$,
10627	FooGallery,
10628	FooGallery.utils,
10629	FooGallery.utils.is
10630);
10631(function($, _, _utils, _is){
10632
10633	_.Dots = _.Paging.extend({});
10634
10635	_.DotsControl = _.PagingControl.extend({
10636		construct: function(template, parent, position){
10637			this._super(template, parent, position);
10638			this.$list = null;
10639			this.$items = null;
10640		},
10641		create: function(){
10642			var self = this;
10643			if (self._super()){
10644				var cls = self.pages.cls, il8n = self.pages.il8n,
10645					items = [], $list = $("<ul/>", {"class": cls.list});
10646
10647				for (var i = 0, l = self.pages.total, $item; i < l; i++){
10648					items.push($item = self.createItem(i + 1, il8n.page));
10649					$list.append($item);
10650				}
10651				self.$list = $list;
10652				self.$items = $($.map(items, function($item){ return $item.get(); }));
10653				self.$container.append($list);
10654				return true;
10655			}
10656			return false;
10657		},
10658		destroy: function(){
10659			var self = this, sel = self.pages.sel;
10660			self.$list.find(sel.link).off("click.foogallery", self.onLinkClick);
10661			self.$list = $();
10662			self.$items = $();
10663			self._super();
10664		},
10665		update: function(pageNumber){
10666			this.setSelected(pageNumber - 1);
10667		},
10668		setSelected: function(index){
10669			var self = this, cls = self.pages.cls, il8n = self.pages.il8n, sel = self.pages.sel;
10670			// first find any previous selected items and deselect them
10671			self.$items.filter(sel.selected).removeClass(cls.selected).each(function (i, el) {
10672				// 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
10673				var $item = $(el), label = $item.data("label"), $sr = $item.find(sel.reader);
10674				// if we have an original value and a screen-reader element then update it
10675				if (_is.string(label) && $sr.length !== 0) {
10676					$sr.html(label);
10677				}
10678			});
10679			// next find the newly selected item and set it as selected
10680			self.$items.eq(index).addClass(cls.selected).each(function (i, el) {
10681				// we need to update the items screen-reader text to appropriately show it as selected using the value of the labels.current option
10682				var $item = $(el), $sr = $item.find(sel.reader), label = $sr.html();
10683				// if we have a current label to backup and a screen-reader element then update it
10684				if (_is.string(label) && $sr.length !== 0) {
10685					// store the original screen-reader text so we can revert it later
10686					$item.data("label", label);
10687					$sr.html(il8n.current);
10688				}
10689			});
10690		},
10691		/**
10692		 * @summary Create and return a jQuery object containing a single `li` and its' link.
10693		 * @memberof FooGallery.DotsControl#
10694		 * @function createItem
10695		 * @param {(number|string)} pageNumber - The page number for the item.
10696		 * @param {string} [label=""] - The label that is displayed when hovering over an item.
10697		 * @param {string} [text=""] - The text to display for the item, if not supplied this defaults to the `pageNumber` value.
10698		 * @param {string} [classNames=""] - A space separated list of CSS class names to apply to the item.
10699		 * @param {string} [sr=""] - The text to use for screen readers, if not supplied this defaults to the `label` value.
10700		 * @returns {jQuery}
10701		 */
10702		createItem: function(pageNumber, label, text, classNames, sr){
10703			text = _is.string(text) ? text : pageNumber;
10704			label = _is.string(label) ? label : "";
10705			var self = this, opt = self.pages.opt, cls = self.pages.cls;
10706			var $link = $("<a/>", {"class": cls.link, "href": "#page-" + pageNumber}).html(text).on("click.foogallery", {self: self, page: pageNumber}, self.onLinkClick);
10707			if (!_is.empty(label)){
10708				$link.attr("title", label.replace(/\{PAGE}/g, pageNumber).replace(/\{LIMIT}/g, opt.limit + ""));
10709			}
10710			sr = _is.string(sr) ? sr : label;
10711			if (!_is.empty(sr)){
10712				$link.prepend($("<span/>", {"class":cls.reader, text: sr.replace(/\{PAGE}/g, "").replace(/\{LIMIT}/g, opt.limit + "")}));
10713			}
10714			var $item = $("<li/>", {"class": cls.item}).append($link);
10715			classNames = _is.string(classNames) ? classNames : "";
10716			if (!_is.empty(classNames)){
10717				$item.addClass(classNames);
10718			}
10719			return $item;
10720		},
10721		/**
10722		 * @summary Handles the click event of the dots links.
10723		 * @memberof FooGallery.DotsControl#
10724		 * @function onLinkClick
10725		 * @param {jQuery.Event} e - The jQuery.Event object for the click event.
10726		 * @private
10727		 */
10728		onLinkClick: function(e){
10729			e.preventDefault();
10730			var self = e.data.self, page = e.data.page, sel = self.pages.sel;
10731			// 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
10732			if (!$(this).closest(sel.item).is(sel.disabled)){
10733				self.pages.set(page, true);
10734			}
10735		}
10736	});
10737
10738	_.paging.register("dots", _.Dots, _.DotsControl, {
10739		type: "dots",
10740		position: "both",
10741		pushOrReplace: "push"
10742	}, {
10743		list: "fg-dots",
10744		item: "fg-dot-item",
10745		link: "fg-dot-link",
10746		disabled: "fg-disabled",
10747		selected: "fg-selected",
10748		visible: "fg-visible",
10749		reader: "fg-sr-only"
10750	}, {
10751		current: "Current page",
10752		page: "Page {PAGE}"
10753	});
10754
10755})(
10756	FooGallery.$,
10757	FooGallery,
10758	FooGallery.utils,
10759	FooGallery.utils.is
10760);
10761(function($, _, _utils, _is){
10762
10763	_.Pagination = _.Dots.extend({
10764		construct: function(template){
10765			this._super(template);
10766			this.limit = this.opt.limit;
10767			this.showFirstLast = this.opt.showFirstLast;
10768			this.showPrevNext = this.opt.showPrevNext;
10769			this.showPrevNextMore = this.opt.limit === 0 ? false : this.opt.showPrevNextMore;
10770			this.pageKeywords = ["first","prev","prevMore","nextMore","next","last"];
10771			this.sel.firstPrev = [this.sel.first, this.sel.prev].join(",");
10772			this.sel.nextLast = [this.sel.next, this.sel.last].join(",");
10773			this.range = {
10774				index: -1,
10775				start: -1,
10776				end: -1,
10777				changed: false,
10778				selected: false
10779			};
10780		},
10781		build: function(){
10782			this._super();
10783			this.range = {
10784				index: -1,
10785				start: -1,
10786				end: -1,
10787				changed: false,
10788				selected: false
10789			};
10790		},
10791		controls: function(pageNumber){
10792			var self = this;
10793			if (self.isValid(pageNumber)){
10794				self.range = self.getControlRange(pageNumber);
10795				$.each(self.ctrls, function(i, control){
10796					control.update(self.range);
10797				});
10798			}
10799		},
10800		isValid: function(pageNumber){
10801			return this._super(pageNumber) || this.isKeyword(pageNumber);
10802		},
10803		isKeyword: function(pageNumber){
10804			return _is.string(pageNumber) && _utils.inArray(pageNumber, this.pageKeywords) !== -1;
10805		},
10806		number: function(value){
10807			var self = this;
10808			if (value === "first") value = 1;
10809			if (value === "prev") value = self.current - 1;
10810			if (value === "next") value = self.current + 1;
10811			if (value === "last") value = self.total;
10812			if (value === "prevMore" || value === "nextMore") value = self.current;
10813			return self._super(value);
10814		},
10815		getControlRange: function(pageNumber){
10816			var self = this;
10817			switch(pageNumber){
10818				case "prevMore":
10819					return self._range(self.range.start - 1, false, false);
10820				case "nextMore":
10821					return self._range(self.range.end + 1, true, false);
10822				default:
10823					pageNumber = self.number(pageNumber);
10824					return self._range(pageNumber - 1, pageNumber <= self.current)
10825			}
10826		},
10827		_range: function(index, leftMost, selected){
10828			var self = this, range = {
10829				index: index,
10830				start: self.range.start,
10831				end: self.range.end,
10832				changed: false,
10833				selected: _is.boolean(selected) ? selected : true
10834			};
10835			// if we have less pages than the limit or there is no limit
10836			if (self.total <= self.limit || self.limit === 0){
10837				// then set the range so that all page links are displayed
10838				range.start = 0;
10839				range.end = self.total - 1;
10840			}
10841			// else if the goto index falls outside the current range
10842			else if (index < range.start || index > range.end) {
10843				// then calculate the correct range to display
10844				var max = index + (self.limit - 1),
10845					min = index - (self.limit - 1);
10846
10847				// if the goto index is to be displayed as the left most page link
10848				if (leftMost) {
10849					// then check that the right most item falls within the actual number of pages
10850					range.start = index;
10851					range.end = max;
10852					while (range.end > self.total) {
10853						// adjust the visible range so that the right most item is not greater than maximum page
10854						range.start -= 1;
10855						range.end -= 1;
10856					}
10857				}
10858				// else if the goto index is to be displayed as the right most page link
10859				else {
10860					// then check that the left most item falls within the actual number of pages
10861					range.start = min;
10862					range.end = index;
10863					while (range.start < 0) {
10864						// adjust the visible range so that the left most item is not less than the minimum page
10865						range.start += 1;
10866						range.end += 1;
10867					}
10868				}
10869			}
10870			// if the current visible range of links has changed
10871			range.changed = range.start !== self.range.start || range.end !== self.range.end;
10872			if (range.changed){
10873				// then cache the range for the next time this method is called
10874				self.range = range;
10875			}
10876			return range;
10877		}
10878	});
10879
10880	_.PaginationControl = _.DotsControl.extend({
10881		construct: function(template, parent, position){
10882			this._super(template, parent, position);
10883			this.$buttons = $();
10884		},
10885		create: function(){
10886			var self = this;
10887			if (self._super()){
10888				var displayAll = self.pages.total <= self.pages.limit || self.pages.limit === 0,
10889					buttons = [], $button;
10890
10891				if (!displayAll && self.pages.showPrevNextMore){
10892					buttons.push($button = self.createButton("prevMore"));
10893					self.$list.prepend($button);
10894				}
10895				if (self.pages.showPrevNext){
10896					buttons.push($button = self.createButton("prev"));
10897					self.$list.prepend($button);
10898				}
10899				if (self.pages.showFirstLast){
10900					buttons.push($button = self.createButton("first"));
10901					self.$list.prepend($button);
10902				}
10903				if (!displayAll && self.pages.showPrevNextMore){
10904					buttons.push($button = self.createButton("nextMore"));
10905					self.$list.append($button);
10906				}
10907				if (self.pages.showPrevNext){
10908					buttons.push($button = self.createButton("next"));
10909					self.$list.append($button);
10910				}
10911				if (self.pages.showFirstLast){
10912					buttons.push($button = self.createButton("last"));
10913					self.$list.append($button);
10914				}
10915				self.$buttons = $($.map(buttons, function($button){ return $button.get(); }));
10916
10917				return true;
10918			}
10919			return false;
10920		},
10921		destroy: function(){
10922			this.$buttons = null;
10923			this._super();
10924		},
10925		update: function(range){
10926			var self = this, sel = self.pages.sel;
10927			// if the range changed update the visible links
10928			if (range.changed) {
10929				self.setVisible(range.start, range.end);
10930			}
10931			// if the range index is selected
10932			if (range.selected) {
10933				// then update the items as required
10934				self.setSelected(range.index);
10935
10936				// if this is the first page then we need to disable the first and prev buttons
10937				self.toggleDisabled(self.$buttons.filter(sel.firstPrev), range.index <= 0);
10938				// if this is the last page we need to disable the next and last buttons
10939				self.toggleDisabled(self.$buttons.filter(sel.nextLast), range.index >= self.pages.total - 1);
10940			}
10941			// if the visible range starts with the first page then we need to disable the prev more button
10942			self.toggleDisabled(self.$buttons.filter(sel.prevMore), range.start <= 0);
10943			// if the visible range ends with the last page then we need to disable the next more button
10944			self.toggleDisabled(self.$buttons.filter(sel.nextMore), range.end >= self.pages.total - 1);
10945		},
10946		setVisible: function(start, end){
10947			var self = this, cls = self.pages.cls;
10948			// when we slice we add + 1 to the upper limit of the range as $.slice does not include the end index in the result
10949			self.$items.removeClass(cls.visible).slice(start, end + 1).addClass(cls.visible);
10950		},
10951		toggleDisabled: function($buttons, state){
10952			var self = this, cls = self.pages.cls, sel = self.pages.sel;
10953			if (state) {
10954				$buttons.addClass(cls.disabled).find(sel.link).attr("tabindex", -1);
10955			} else {
10956				$buttons.removeClass(cls.disabled).find(sel.link).removeAttr("tabindex");
10957			}
10958		},
10959		/**
10960		 * @summary Create and return a jQuery object containing a single `li` and its' button.
10961		 * @memberof FooGallery.PaginationControl#
10962		 * @function createButton
10963		 * @param {string} keyword - One of the page keywords; `"first"`, `"prev"`, `"prevMore"`, `"nextMore"`, `"next"` or `"last"`.
10964		 * @returns {jQuery}
10965		 */
10966		createButton: function(keyword){
10967			var self = this, cls = self.pages.cls, il8n = self.pages.il8n;
10968			return self.createItem(keyword, il8n.labels[keyword], il8n.buttons[keyword], cls.button + " " + cls[keyword]);
10969		}
10970	});
10971
10972	_.paging.register("pagination", _.Pagination, _.PaginationControl, {
10973		type: "pagination",
10974		position: "both",
10975		pushOrReplace: "push",
10976		limit: 5,
10977		showPrevNext: true,
10978		showFirstLast: true,
10979		showPrevNextMore: true
10980	}, {
10981		list: "fg-pages",
10982		item: "fg-page-item",
10983		button: "fg-page-button",
10984		link: "fg-page-link",
10985		first: "fg-page-first",
10986		prev: "fg-page-prev",
10987		prevMore: "fg-page-prev-more",
10988		nextMore: "fg-page-next-more",
10989		next: "fg-page-next",
10990		last: "fg-page-last",
10991		disabled: "fg-disabled",
10992		selected: "fg-selected",
10993		visible: "fg-visible",
10994		reader: "fg-sr-only"
10995	}, {
10996		buttons: {
10997			first: "&laquo;",
10998			prev: "&lsaquo;",
10999			next: "&rsaquo;",
11000			last: "&raquo;",
11001			prevMore: "&hellip;",
11002			nextMore: "&hellip;"
11003		},
11004		labels: {
11005			current: "Current page",
11006			page: "Page {PAGE}",
11007			first: "First page",
11008			prev: "Previous page",
11009			next: "Next page",
11010			last: "Last page",
11011			prevMore: "Show previous {LIMIT} pages",
11012			nextMore: "Show next {LIMIT} pages"
11013		}
11014	});
11015
11016})(
11017	FooGallery.$,
11018	FooGallery,
11019	FooGallery.utils,
11020	FooGallery.utils.is
11021);
11022(function($, _, _utils, _is, _obj, _fn, _t){
11023
11024    var instance_id = 0;
11025
11026    /**
11027     * @memberof FooGallery.
11028     * @class Panel
11029     * @param template
11030     * @param options
11031     * @param classes
11032     * @param il8n
11033     * @augments FooGallery.Component
11034     */
11035    _.Panel = _.Component.extend(/** @lends FooGallery.Panel */{
11036        /**
11037         * @constructs
11038         * @param template
11039         * @param options
11040         * @param classes
11041         * @param il8n
11042         */
11043        construct: function(template, options, classes, il8n){
11044            var self = this;
11045            self.instanceId = ++instance_id;
11046            self._super(template);
11047
11048            self.opt = _obj.extend({}, self.tmpl.opt.panel, options);
11049
11050            self.cls = _obj.extend({}, self.tmpl.cls.panel, classes);
11051
11052            self.il8n = _obj.extend({}, self.tmpl.il8n.panel, il8n);
11053
11054            var states = self.cls.states;
11055            self.cls.states.all = Object.keys(states).map(function (key) {
11056                return states[key];
11057            }).join(" ");
11058            self.cls.states.allLoading = [states.idle, states.loading, states.loaded, states.error].join(" ");
11059            self.cls.states.allProgress = [states.idle, states.started, states.stopped, states.paused].join(" ");
11060
11061            self.sel = _utils.selectify(self.cls);
11062
11063            self.videoSources = !_is.undef(_.Panel.Video) ? _.Panel.Video.sources.load(self) : [];
11064
11065            self.buttons = new _.Panel.Buttons(self);
11066
11067            self.content = new _.Panel.Content(self);
11068            self.info = new _.Panel.Info(self);
11069            self.thumbs = new _.Panel.Thumbs(self);
11070
11071            self.areas = [self.content, self.info, self.thumbs];
11072
11073            if ( _.Panel.Cart ){
11074                self.cart = new _.Panel.Cart(self);
11075                self.areas.push( self.cart );
11076            }
11077
11078            if ( _.Panel.Comments ){
11079                self.comments = new _.Panel.Comments(self);
11080                self.areas.push( self.comments );
11081            }
11082
11083            // Make sure only one side area can occupy the same position, overrides supplied options
11084            // There can be only one...
11085            const highlanders = self.areas.filter( a => a instanceof _.Panel.SideArea && a.isVisible && a.opt.group === 'overlay' );
11086            while ( highlanders.length > 0 ) {
11087                const theOne = highlanders.shift();
11088                highlanders.forEach( challenger => {
11089                    if ( challenger.isTargetingSamePosition( theOne ) ) {
11090                        challenger.isVisible = false;
11091                        challenger.button.isPressed = false;
11092                    }
11093                } );
11094            }
11095
11096            self.$el = null;
11097
11098            self.el = null;
11099
11100            self.isCreated = false;
11101
11102            self.isDestroyed = false;
11103
11104            self.isDestroying = false;
11105
11106            self.isAttached = false;
11107
11108            self.isLoading = false;
11109
11110            self.isLoaded = false;
11111
11112            self.isError = false;
11113
11114            self.isInline = false;
11115
11116            self.isMaximized = false;
11117
11118            self.isFullscreen = false;
11119
11120            self.hasTransition = !_is.empty(self.cls.transition[self.opt.transition]);
11121
11122            self.currentItem = null;
11123
11124            self.prevItem = null;
11125
11126            self.nextItem = null;
11127
11128            self.lastBreakpoint = null;
11129
11130            self.isSmallScreen = false;
11131            self.isMediumScreen = false;
11132            self.isLargeScreen = false;
11133            self.isMobileLayout = false;
11134
11135            self.breakpointClassNames = self.opt.breakpoints.map(function(bp){
11136                return "fg-" + bp.name + " fg-" + bp.name + "-width" + " fg-" + bp.name + "-height";
11137            }).concat(["fg-landscape","fg-portrait"]).join(" ");
11138
11139            self.robserver = new ResizeObserver(_fn.throttle(function (entries) {
11140                if (!self.destroying && !self.destroyed){
11141                    entries.forEach(function (entry) {
11142                        if (entry.target === self.el){
11143                            var size = _utils.getResizeObserverSize(entry);
11144                            self.onResize(size.width, size.height);
11145                        }
11146                    });
11147                }
11148            }, 50));
11149
11150            self.__media = {};
11151
11152            self.__loading = null;
11153
11154            if (!(self.tmpl.destroying || self.tmpl.destroyed)){
11155                self.tmpl.on({
11156                    "after-filter-change": self.onItemsChanged
11157                }, self);
11158            }
11159        },
11160        isVisible: function(item){
11161            return item instanceof _.Item && !item.noLightbox && !item.panelHide;
11162        },
11163        onItemsChanged: function(e){
11164            var self = this;
11165            if (self.thumbs.isCreated && self.tmpl.initialized){
11166                self.thumbs.doCreateThumbs(self.tmpl.items.available(self.isVisible));
11167                if (self.isAttached) self.load(self.tmpl.items.first(self.isVisible));
11168            }
11169        },
11170        create: function(){
11171            var self = this;
11172            if (!self.isCreated) {
11173                var e = self.trigger("create");
11174                if (!e.isDefaultPrevented()) {
11175                    self.isCreated = self.doCreate();
11176                }
11177                if (self.isCreated) {
11178                    self.trigger("created");
11179                }
11180            }
11181            return self.isCreated;
11182        },
11183        doCreate: function(){
11184            var self = this;
11185            self.$el = self.createElem();
11186            self.el = self.$el.get(0);
11187            if (self.tmpl.opt.protected){
11188                self.el.oncontextmenu = function(e){
11189                    e.preventDefault();
11190                    return false;
11191                };
11192            }
11193            if (self.opt.keyboard){
11194                self.$el.attr("tabindex", -1).on("keydown.foogallery", {self: self}, self.onKeyDown);
11195            }
11196            self.buttons.appendTo( self.$el );
11197            self.areas.forEach(function(area){
11198                area.appendTo( self.$el );
11199            });
11200            return true;
11201        },
11202        createElem: function(){
11203            var self = this, transition = self.cls.transition[self.opt.transition] || "";
11204            self.hasTransition = !_is.empty(transition);
11205            var classes = [
11206                self.cls.elem,
11207                transition,
11208                self.cls.buttons.portrait[self.opt.buttonsPortrait] || "",
11209                self.cls.buttons.landscape[self.opt.buttonsLandscape] || "",
11210                _is.string(self.opt.theme) ? self.opt.theme : self.tmpl.getCSSClass("theme", "fg-dark"),
11211                _is.string(self.opt.loadingIcon) ? self.opt.loadingIcon : self.tmpl.getCSSClass("loadingIcon"),
11212                _is.string(self.opt.hoverIcon) ? self.opt.hoverIcon : self.tmpl.getCSSClass("hoverIcon"),
11213                _is.string(self.opt.videoIcon) ? self.opt.videoIcon : self.tmpl.getCSSClass("videoIcon"),
11214                _is.boolean(self.opt.stickyVideoIcon) && self.opt.stickyVideoIcon ? self.cls.stickyVideoIcon : self.tmpl.getCSSClass("stickyVideoIcon"),
11215                _is.string(self.opt.insetShadow) ? self.opt.insetShadow : self.tmpl.getCSSClass("insetShadow"),
11216                _is.string(self.opt.filter) ? self.opt.filter : self.tmpl.getCSSClass("filter"),
11217                _is.string(self.opt.hoverColor) ? self.opt.hoverColor : self.tmpl.getCSSClass("hoverColor"),
11218                _is.boolean(self.opt.hoverScale) && self.opt.hoverScale ? self.cls.hoverScale : self.tmpl.getCSSClass("hoverScale"),
11219                _is.string(self.opt.button) ? self.opt.button : "",
11220                _is.string(self.opt.highlight) ? self.opt.highlight : "",
11221                self.opt.adjustBackgroundColor ? self.cls.dominantColor : "",
11222                self.opt.stackSideAreas ? self.cls.stackSideAreas : "",
11223                self.opt.preserveButtonSpace ? self.cls.preserveButtonSpace : "",
11224                self.opt.fitMedia ? self.cls.fitMedia : "",
11225                self.opt.noMobile ? self.cls.noMobile : "",
11226                self.opt.hoverButtons ? self.cls.hoverButtons : "",
11227                self.opt.classNames
11228            ];
11229            return $('<div/>
11229').addClass(classes.join(" "));
11230        },
11231        destroy: function () {
11232            var self = this, _super = self._super.bind(self);
11233            if (self.isDestroyed) return _fn.resolved;
11234            self.isDestroying = true;
11235            return $.Deferred(function (def) {
11236                if (self.isLoading && _is.promise(self.__loading)) {
11237                    self.__loading.always(function () {
11238                        var e = self.trigger("destroy");
11239                        self.isDestroying = false;
11240                        if (!e.isDefaultPrevented()) {
11241                            self.isDestroyed = self.doDestroy();
11242                        }
11243                        if (self.isDestroyed) {
11244                            self.trigger("destroyed");
11245                        }
11246                        def.resolve();
11247                    });
11248                } else {
11249                    var e = self.trigger("destroy");
11250                    self.isDestroying = false;
11251                    if (!e.isDefaultPrevented()) {
11252                        self.isDestroyed = self.doDestroy();
11253                    }
11254                    if (self.isDestroyed) {
11255                        self.trigger("destroyed");
11256                    }
11257                    def.resolve();
11258                }
11259            }).then(function(){
11260                _super();
11261            }).promise();
11262        },
11263        doDestroy: function(){
11264            var self = this;
11265            self.buttons.destroy();
11266            self.areas.reverse();
11267            self.areas.forEach(function (area) {
11268                area.destroy();
11269            });
11270            self.detach();
11271            if (self.isCreated){
11272                self.$el.remove();
11273            }
11274            return true;
11275        },
11276        appendTo: function( parent ){
11277            var self = this;
11278            if ((self.isCreated || self.create()) && !self.isAttached){
11279                var e = self.trigger("append", [parent]);
11280                if (!e.isDefaultPrevented()) {
11281                    self.isAttached = self.doAppendTo( parent );
11282                }
11283                if (self.isAttached) {
11284                    self.trigger("appended", [parent]);
11285                }
11286            }
11287            return self.isAttached;
11288        },
11289        doAppendTo: function( parent ){
11290            var self = this, $parent = $( parent ), maximize = self.buttons.get("maximize");
11291            self.isInline = !$parent.is("body");
11292            self.$el.appendTo( $parent );
11293
11294            maximize.set(!self.isInline, self.isInline && maximize.isEnabled());
11295
11296            self.robserver.observe(self.el);
11297
11298            self.areas.forEach(function (area) {
11299                area.listen();
11300            });
11301            return self.el.parentNode !== null;
11302        },
11303        detach: function(){
11304            var self = this;
11305            if (self.isCreated && self.isAttached) {
11306                var e = self.trigger("detach");
11307                if (!e.isDefaultPrevented()) {
11308                    self.isAttached = !self.doDetach();
11309                }
11310                if (!self.isAttached) {
11311                    self.trigger("detached");
11312                }
11313            }
11314            return !self.isAttached;
11315        },
11316        doDetach: function(){
11317            var self = this;
11318            self.robserver.unobserve(self.el);
11319            self.areas.forEach(function (area) {
11320                area.stopListening();
11321            });
11322            self.$el.detach();
11323            return true;
11324        },
11325        resize: function(){
11326            var self = this;
11327            self.$el.removeClass(self.breakpointClassNames).addClass(self.lastBreakpoint);
11328            self.isMediumScreen = self.$el.hasClass("fg-medium");
11329            self.isLargeScreen = self.$el.hasClass("fg-large");
11330            self.isXLargeScreen = self.$el.hasClass("fg-x-large");
11331            self.isSmallScreen = !self.isMediumScreen && !self.isLargeScreen && !self.isXLargeScreen;
11332            self.isMobileLayout = self.isSmallScreen && !self.opt.noMobile;
11333            self.areas.forEach(function (area) {
11334                area.resize();
11335            });
11336            self.buttons.resize();
11337        },
11338        onResize: function(width, height){
11339            var self = this, bp = self.getBreakpoint(width, height);
11340            if (self.lastBreakpoint !== bp){
11341                self.lastBreakpoint = bp;
11342                self.resize();
11343            }
11344        },
11345        getBreakpoint: function(width, height){
11346            var self = this,
11347                result = [];
11348
11349            self.opt.breakpoints.forEach(function(bp){
11350                var w = bp.width <= width, h = bp.height <= height;
11351                if (w && h) result.push("fg-" + bp.name);
11352                if (w) result.push("fg-" + bp.name + "-width");
11353                if (h) result.push("fg-" + bp.name + "-height");
11354            });
11355
11356            result.push(width > height ? "fg-landscape" : "fg-portrait");
11357
11358            return result.length > 0 ? result.join(" ") : null;
11359        },
11360        getMedia: function(item){
11361            if (!(item instanceof _.Item)) return null;
11362            if (this.__media.hasOwnProperty(item.id)) return this.__media[item.id];
11363            return this.__media[item.id] = _.Panel.media.make(item.type, this, item);
11364        },
11365        getItem: function(item){
11366            var self = this, result = item;
11367            if (!(result instanceof _.Item)) result = self.currentItem;
11368            if (!(result instanceof _.Item)) result = self.tmpl.items.first(self.isVisible);
11369            if (item instanceof _.Item && !self.isVisible(item)){
11370                result = self.tmpl.items.next(item, self.isVisible, self.opt.loop);
11371                if (!(result instanceof _.Item)){
11372                    result = self.tmpl.items.prev(item, self.isVisible, self.opt.loop);
11373                }
11374            }
11375            return result;
11376        },
11377        load: function( item ){
11378            var self = this;
11379
11380            item = self.getItem(item);
11381
11382            if (!(item instanceof _.Item)) return _fn.reject("no item to load");
11383            if (item === self.currentItem) return _fn.reject("item is currently loaded");
11384
11385            self.isLoading = true;
11386            self.isLoaded = false;
11387            self.isError = false;
11388
11389            return self.__loading = $.Deferred(function(def){
11390                if (!self.isCreated || !self.isAttached){
11391                    def.rejectWith("not created or attached");
11392                    return;
11393                }
11394                var media = self.getMedia(item);
11395                if (!(media instanceof _.Panel.Media)){
11396                    def.rejectWith("no media to load");
11397                    return;
11398                }
11399                var e = self.trigger("load", [media, item]);
11400                if (e.isDefaultPrevented()){
11401                    def.rejectWith("default prevented");
11402                    return;
11403                }
11404                self.currentItem = item;
11405                self.prevItem = self.tmpl.items.prev(item, self.isVisible, self.opt.loop);
11406                self.nextItem = self.tmpl.items.next(item, self.isVisible, self.opt.loop);
11407                self.updateBackgroundColor(item);
11408                self.doLoad(media).then(def.resolve).fail(def.reject);
11409            }).always(function(){
11410                self.isLoading = false;
11411            }).then(function(){
11412                self.isLoaded = true;
11413                self.trigger("loaded", [item]);
11414                item.updateState();
11415            }).fail(function(){
11416                self.isError = true;
11417                self.trigger("error", [item]);
11418            }).promise();
11419        },
11420        updateBackgroundColor: function( item ){
11421            var self = this, color = "";
11422            if (!self.opt.adjustBackgroundColor || !self.el) return;
11423            if (item instanceof _.Item && _is.element(item.el)){
11424                color = item.el.style.getPropertyValue("--fg-lightbox-dominant-color");
11425                if (_is.empty(color) && window.getComputedStyle){
11426                    color = window.getComputedStyle(item.el).getPropertyValue("--fg-lightbox-dominant-color");
11427                }
11428            }
11429            color = _is.string(color) ? color.trim() : "";
11430            if (_is.empty(color)){
11431                self.el.style.removeProperty("--fg-dominant-color");
11432            } else {
11433                self.el.style.setProperty("--fg-dominant-color", color);
11434            }
11435        },
11436        doLoad: function( media ){
11437            var self = this, wait = [];
11438            self.buttons.beforeLoad(media);
11439            self.areas.forEach(function (area) {
11440                wait.push(area.load(media));
11441            });
11442            return $.when.apply($, wait).then(function(){
11443                self.buttons.afterLoad(media);
11444            }).promise();
11445        },
11446        open: function( item, parent ){
11447            var self = this;
11448            item = self.getItem(item);
11449            var e = self.trigger("open", [item, parent]);
11450            if (e.isDefaultPrevented()) return _fn.reject("default prevented");
11451            return self.doOpen(item, parent).then(function(){
11452                self.trigger("opened", [item, parent]);
11453            });
11454        },
11455        doOpen: function( item, parent ){
11456            var self = this;
11457            return $.Deferred(function(def){
11458                if (!(item instanceof _.Item)){
11459                    def.rejectWith("item not instanceof FooGallery.Item");
11460                    return;
11461                }
11462                parent = !_is.empty(parent) ? parent : "body";
11463                if (!self.isAttached){
11464                    self.appendTo( parent );
11465                }
11466                if (self.isAttached){
11467                    self.load( item ).then(def.resolve).fail(def.reject);
11468                } else {
11469                    def.rejectWith("not attached");
11470                }
11471            }).promise();
11472        },
11473        next: function(){
11474            var self = this, current = self.currentItem, next = self.nextItem;
11475            if (!(next instanceof _.Item)) return _fn.reject("no next item");
11476            var e = self.trigger("next", [current, next]);
11477            if (e.isDefaultPrevented()) return _fn.reject("default prevented");
11478            return self.doNext(next).then(function(){
11479                self.trigger("after-next", [current, next]);
11480            });
11481        },
11482        doNext: function(item){
11483            return this.load( item );
11484        },
11485        prev: function(){
11486            var self = this, current = self.currentItem, prev = self.prevItem;
11487            if (!(prev instanceof _.Item)) return _fn.reject("no prev item");
11488            var e = self.trigger("prev", [current, prev]);
11489            if (e.isDefaultPrevented()) return _fn.reject("default prevented");
11490            return self.doPrev(prev).then(function(){
11491                self.trigger("after-prev", [current, prev]);
11492            });
11493        },
11494        doPrev: function(item){
11495            return this.load( item );
11496        },
11497        close: function(immediate){
11498            var self = this;
11499            if ( self.isClosing ) return $.Deferred().reject();
11500            var e = self.trigger("close", [self.currentItem]);
11501            if (e.isDefaultPrevented()) return _fn.reject("default prevented");
11502            return self.doClose(immediate).then(function(){
11503                self.trigger("closed");
11504            });
11505        },
11506        doClose: function(immediate, detach){
11507            detach = _is.boolean(detach) ? detach : true;
11508            var self = this;
11509            self.isClosing = true;
11510            return $.Deferred(function(def){
11511                var closeArea = function(area){
11512                    try {
11513                        return area.close(immediate);
11514                    } catch (err){
11515                        return _fn.reject(err);
11516                    }
11517                };
11518                $.when(closeArea(self.content)).always(function(){
11519                    var wait = [];
11520                    self.areas.forEach(function(area){
11521                        if (area !== self.content){
11522                            wait.push(closeArea(area));
11523                        }
11524                    });
11525                    _fn.allSettled(wait).then(function(){
11526                        def.resolve();
11527                    });
11528                });
11529            }).always(function(){
11530                self.isClosing = false;
11531                self.currentItem = null;
11532                if (self.opt.adjustBackgroundColor && self.el){
11533                    self.el.style.removeProperty("--fg-dominant-color");
11534                }
11535                self.buttons.close();
11536                if (detach) self.detach();
11537                self.tmpl.state.clear();
11538            }).promise();
11539        },
11540        trapFocus: function(){
11541            if (!this.isCreated) return;
11542            this.$el.on('keydown', {self: this}, this.onTrapFocusKeydown);
11543        },
11544        releaseFocus: function(){
11545            if (!this.isCreated) return;
11546            this.$el.off('keydown', this.onTrapFocusKeydown);
11547        },
11548        onTrapFocusKeydown: function(e){
11549            // If TAB key pressed
11550            if (e.keyCode === 9) {
11551                var self = e.data.self,
11552                    $target = $(e.target),
11553                    targetIsDialog = $target.is('[role=dialog]'),
11554                    $dialog = targetIsDialog ? $target : $target.parents('[role=dialog]');
11555
11556                // If inside a Modal dialog (determined by attribute role="dialog")
11557                if ($dialog.length) {
11558                    // Find first or last input element in the dialog parent (depending on whether Shift was pressed).
11559                    var $focusable = $dialog.find(self.opt.focusable.include).not(self.opt.focusable.exclude),
11560                        $first = $focusable.first(), $last = $focusable.last(),
11561                        $boundary = e.shiftKey ? $first : $last,
11562                        $new = e.shiftKey ? $last : $first;
11563
11564                    if ($boundary.length && ( targetIsDialog || $target.is($boundary) )) {
11565                        e.preventDefault();
11566                        $new.trigger('focus');
11567                    }
11568                }
11569            }
11570        },
11571        onKeyDown: function(e){
11572            var self = e.data.self;
11573            switch (e.which){
11574                case 39: case 40: self.next(); break;
11575                case 37: case 38: self.prev(); break;
11576                case 27:
11577                    var button;
11578                    if (self.isFullscreen){
11579                        button = self.buttons.get("fullscreen");
11580                        button.exit();
11581                    } else if (self.isMaximized && self.isInline){
11582                        button = self.buttons.get("maximize");
11583                        button.exit();
11584                    } else if (self.opt.buttons.close) {
11585                        self.close();
11586                    }
11587                    break;
11588            }
11589        }
11590    });
11591
11592
11593    _.template.configure("core", {
11594        panel: {
11595            classNames: "",
11596            noMobile: false,
11597            hoverButtons: false,
11598            icons: "default",
11599            transition: "none", // none | fade | horizontal | vertical
11600
11601            // the below are CSS class names to use, if not supplied the lightbox will inherit the value from the gallery
11602            theme: null,
11603            button: null,
11604            highlight: null,
11605            loadingIcon: null,
11606            hoverIcon: null,
11607            videoIcon: null,
11608            stickyVideoIcon: null,
11609            hoverColor: null,
11610            hoverScale: null,
11611            insetShadow: null,
11612            filter: null,
11613
11614            loop: true,
11615            autoProgress: 0,
11616            autoProgressStart: true,
11617            autoProgressVisible: true,
11618            fitMedia: false,
11619            adjustBackgroundColor: false,
11620            keyboard: true,
11621            noScrollbars: true,
11622            swipe: true,
11623            stackSideAreas: true,
11624            preserveButtonSpace: true,
11625            buttonsPortrait: "top", // top | bottom
11626            buttonsLandscape: "right", // left | right
11627            admin: false,
11628
11629            info: "bottom", // none | top | bottom | left | right
11630            infoVisible: false,
11631            infoOverlay: true,
11632            infoAutoHide: true,
11633            infoAlign: "default", // default | left | center | right | justified
11634            exif: "none", // none | full | partial | minimal
11635
11636            cart: "none", // none | top | bottom | left | right
11637            cartAutoHide: true,
11638            cartVisible: false,
11639            cartOriginal: false,
11640            cartOverlay: true,
11641            cartAjax: null,
11642            cartNonce: null,
11643            cartTimeout: null,
11644
11645            thumbs: "none", // none | top | bottom | left | right
11646            thumbsVisible: true,
11647            thumbsCaptions: true,
11648            thumbsCaptionsAlign: "default", // default | left | center | right | justified
11649            thumbsSmall: false,
11650            thumbsBestFit: true,
11651
11652            focusable: {
11653                include: 'a[href], area[href], input, select, textarea, button, iframe, object, embed, [tabindex], [contenteditable]',
11654                exclude: '[tabindex=-1], [disabled], :hidden'
11655            },
11656
11657            buttons: {
11658                prev: true,
11659                next: true,
11660                close: true,
11661                maximize: true,
11662                fullscreen: true,
11663                autoProgress: true,
11664                info: true,
11665                thumbs: false,
11666                cart: true,
11667                download: false
11668            },
11669            breakpoints: [{
11670                name: "medium",
11671                width: 800,
11672                height: 800
11673            },{
11674                name: "large",
11675                width: 1024,
11676                height: 1024
11677            },{
11678                name: "x-large",
11679                width: 1280,
11680                height: 1280
11681            }]
11682        }
11683    },{
11684        panel: {
11685            elem: "fg-panel",
11686            maximized: "fg-panel-maximized",
11687            fullscreen: "fg-panel-fullscreen",
11688
11689            fitMedia: "fg-panel-fit-media",
11690            noScrollbars: "fg-panel-no-scroll",
11691            stackSideAreas: "fg-panel-area-stack",
11692            preserveButtonSpace: "fg-panel-preserve-button-space",
11693            hoverButtons: "fg-panel-hover-buttons",
11694            stickyVideoIcon: "fg-video-sticky",
11695            hoverScale: "fg-hover-scale",
11696            noMobile: "fg-panel-no-mobile",
11697            dominantColor: "fg-dominant-color",
11698
11699            loader: "fg-loader",
11700
11701            states: {
11702                idle: "fg-idle",
11703                loading: "fg-loading",
11704                loaded: "fg-loaded",
11705                error: "fg-error",
11706                visible: "fg-visible",
11707                reverse: "fg-reverse",
11708                toggled: "fg-toggled",
11709                selected: "fg-selected",
11710                disabled: "fg-disabled",
11711                hidden: "fg-hidden",
11712                started: "fg-started",
11713                stopped: "fg-stopped",
11714                paused: "fg-paused",
11715                noTransitions: "fg-no-transitions"
11716            },
11717
11718            buttons: {
11719                portrait: {
11720                    top: "fg-panel-buttons-top",
11721                    bottom: "fg-panel-buttons-bottom"
11722                },
11723                landscape: {
11724                    right: "fg-panel-buttons-right",
11725                    left: "fg-panel-buttons-left"
11726                },
11727                container: "fg-panel-buttons",
11728                prev: "fg-panel-button fg-panel-button-prev",
11729                next: "fg-panel-button fg-panel-button-next",
11730                autoProgress: "fg-panel-button fg-panel-button-progress",
11731                close: "fg-panel-button fg-panel-button-close",
11732                fullscreen: "fg-panel-button fg-panel-button-fullscreen",
11733                maximize: "fg-panel-button fg-panel-button-maximize",
11734                info: "fg-panel-button fg-panel-button-info",
11735                thumbs: "fg-panel-button fg-panel-button-thumbs",
11736                cart: "fg-panel-button fg-panel-button-cart",
11737                download: "fg-panel-button fg-panel-button-download"
11738            },
11739
11740            transition: {
11741                fade: "fg-panel-fade",
11742                horizontal: "fg-panel-horizontal",
11743                vertical: "fg-panel-vertical"
11744            },
11745
11746            area: {
11747                elem: "fg-panel-area",
11748                inner: "fg-panel-area-inner"
11749            },
11750
11751            content: {
11752                buttons: "fg-panel-content-buttons"
11753            },
11754
11755            sideArea: {
11756                toggle: "fg-panel-area-toggle",
11757                button: "fg-panel-area-button",
11758                visible: "fg-panel-area-visible",
11759                overlay: "fg-panel-area-overlay",
11760                position: {
11761                    top: "fg-panel-area-top",
11762                    right: "fg-panel-area-right",
11763                    bottom: "fg-panel-area-bottom",
11764                    left: "fg-panel-area-left"
11765                }
11766            },
11767
11768            info: {
11769                overlay: "fg-panel-info-overlay",
11770                align: {
11771                    left: "fg-panel-media-caption-left",
11772                    center: "fg-panel-media-caption-center",
11773                    right: "fg-panel-media-caption-right",
11774                    justified: "fg-panel-media-caption-justified"
11775                }
11776            },
11777
11778            cart: {
11779                original: "fg-panel-cart-original"
11780            },
11781
11782            thumbs: {
11783                prev: "fg-panel-thumbs-button fg-panel-thumbs-prev",
11784                next: "fg-panel-thumbs-button fg-panel-thumbs-next",
11785                viewport: "fg-panel-thumbs-viewport",
11786                stage: "fg-panel-thumbs-stage",
11787                noCaptions: "fg-panel-thumbs-no-captions",
11788                small: "fg-panel-thumbs-small",
11789                spacer: "fg-panel-thumb-spacer",
11790                thumb: {
11791                    elem: "fg-panel-thumb",
11792                    media: "fg-panel-thumb-media",
11793                    overlay: "fg-panel-thumb-overlay",
11794                    wrap: "fg-panel-thumb-wrap",
11795                    image: "fg-panel-thumb-image",
11796                    caption: "fg-panel-thumb-caption",
11797                    title: "fg-panel-thumb-title",
11798                    description: "fg-panel-thumb-description"
11799                },
11800                align: {
11801                    left: "fg-panel-thumb-caption-left",
11802                    center: "fg-panel-thumb-caption-center",
11803                    right: "fg-panel-thumb-caption-right",
11804                    justified: "fg-panel-thumb-caption-justified"
11805                }
11806            }
11807        }
11808    },{
11809        panel: {
11810            buttons: {
11811                prev: "Previous Media",
11812                next: "Next Media",
11813                close: "Close Modal",
11814                maximize: "Toggle Maximize",
11815                fullscreen: "Toggle Fullscreen",
11816                autoProgress: "Auto Progress",
11817                info: "Toggle Information",
11818                thumbs: "Toggle Thumbnails",
11819                cart: "Toggle Cart",
11820                download: "Download Media"
11821            }
11822        }
11823    });
11824
11825})(
11826    FooGallery.$,
11827    FooGallery,
11828    FooGallery.utils,
11829    FooGallery.utils.is,
11830    FooGallery.utils.obj,
11831    FooGallery.utils.fn,
11832    FooGallery.utils.transition
11833);
11834
11835(function($, _, _icons, _utils, _is){
11836
11837    _.Panel.Buttons = _utils.Class.extend({
11838        construct: function(panel){
11839            this.panel = panel;
11840
11841            this.opt = panel.opt.buttons;
11842
11843            this.cls = panel.cls.buttons;
11844
11845            this.sel = panel.sel.buttons;
11846
11847            this.il8n = panel.il8n.buttons;
11848
11849            this.$el = null;
11850
11851            this.isCreated = false;
11852
11853            this.isAttached = false;
11854
11855            this.__registered = [];
11856
11857            this.registerCore();
11858        },
11859
11860        registerCore: function(){
11861            this.register(new _.Panel.Button(this.panel, "prev", {
11862                icon: "arrow-left",
11863                label: this.il8n.prev,
11864                click: this.panel.prev.bind(this.panel),
11865                beforeLoad: function (media) {
11866                    this.disable(this.panel.prevItem == null);
11867                }
11868            }), 10);
11869            this.register(new _.Panel.Button(this.panel, "next", {
11870                icon: "arrow-right",
11871                label: this.il8n.next,
11872                click: this.panel.next.bind(this.panel),
11873                beforeLoad: function (media) {
11874                    this.disable(this.panel.nextItem == null);
11875                }
11876            }), 20);
11877            this.register(new _.Panel.AutoProgress(this.panel), 30);
11878
11879            // area buttons are inserted by default with priority 99
11880
11881            this.register(new _.Panel.Download(this.panel), 170);
11882            this.register(new _.Panel.Maximize(this.panel), 180);
11883            this.register(new _.Panel.Fullscreen(this.panel), 190);
11884            this.register(new _.Panel.Button(this.panel, "close", {
11885                icon: "close",
11886                label: this.il8n.close,
11887                click: this.panel.close.bind(this.panel)
11888            }), 200);
11889        },
11890
11891        register: function( button, priority ){
11892            if (button instanceof _.Panel.Button){
11893                return this.__registered.push({
11894                    name: button.name,
11895                    button: button,
11896                    priority: _is.number(priority) ? priority : 99
11897                }) - 1;
11898            }
11899            return -1;
11900        },
11901
11902        get: function( name ){
11903            var button = null;
11904            for (var i = 0, l = this.__registered.length; i < l; i++){
11905                if (this.__registered[i].name !== name) continue;
11906                button = this.__registered[i].button;
11907                break;
11908            }
11909            return button;
11910        },
11911
11912        each: function(callback, prioritize){
11913            var self = this;
11914            if (prioritize){
11915                self.__registered.sort(function(a, b){
11916                    return b.priority - a.priority;
11917                });
11918            }
11919            self.__registered.forEach(function(registered, index){
11920                callback.call(self, registered.button, index);
11921            });
11922        },
11923
11924        toggle: function( name, visible ){
11925            var button = this.get(name);
11926            if (button == null) return;
11927            button.toggle(visible);
11928        },
11929
11930        disable: function( name, disable ){
11931            var button = this.get(name);
11932            if (button == null) return;
11933            button.disable(disable);
11934        },
11935
11936        press: function( name, pressed ){
11937            var self = this, button = self.get(name);
11938            if ( pressed && _is.string(button.groupName) ){
11939                self.each(function(btn){
11940                    if ( button !== btn && btn instanceof _.Panel.SideAreaButton && button.groupName === btn.groupName ){
11941                        if ( self.panel.isSmallScreen || btn.isTargetingSamePosition( button ) ) {
11942                            btn.area.toggle( false );
11943                        }
11944                    }
11945                });
11946            }
11947            button.press( pressed );
11948        },
11949
11950        destroy: function(){
11951            var self = this;
11952            var e = self.panel.trigger("buttons-destroy", [self]);
11953            if (!e.isDefaultPrevented()) {
11954                self.isCreated = !self.doDestroy();
11955            }
11956            if (!self.isCreated) {
11957                self.panel.trigger("buttons-destroyed", [self]);
11958            }
11959            return !self.isCreated;
11960        },
11961        doDestroy: function(){
11962            var self = this;
11963            self.each(function(button){
11964                button.destroy();
11965            });
11966            if (self.isCreated){
11967                self.detach();
11968                self.$el.remove();
11969            }
11970            return true;
11971        },
11972        create: function(){
11973            var self = this;
11974            if (!self.isCreated) {
11975                var e = self.panel.trigger("buttons-create", [self]);
11976                if (!e.isDefaultPrevented()) {
11977                    self.isCreated = self.doCreate();
11978                }
11979                if (self.isCreated) {
11980                    self.panel.trigger("buttons-created", [self]);
11981                }
11982            }
11983            return self.isCreated;
11984        },
11985        doCreate: function(){
11986            var self = this;
11987            self.$el = $('<div/>').addClass(self.cls.container);
11988
11989            self.each(function(button){
11990                button.appendTo(self.$el);
11991            }, true);
11992
11993            return true;
11994        },
11995        appendTo: function( parent ){
11996            var self = this;
11997            if (!self.isCreated){
11998                self.create();
11999            }
12000            if (self.isCreated && !self.isAttached){
12001                var e = self.panel.trigger("buttons-append", [self, parent]);
12002                if (!e.isDefaultPrevented()) {
12003                    self.isAttached = self.doAppendTo( parent );
12004                }
12005                if (self.isAttached) {
12006                    self.panel.trigger("buttons-appended", [self, parent]);
12007                }
12008            }
12009            return self.isAttached;
12010        },
12011        doAppendTo: function( parent ){
12012            this.$el.appendTo( parent );
12013            return this.$el.parent().length > 0;
12014        },
12015        detach: function(){
12016            var self = this;
12017            if (self.isCreated && self.isAttached) {
12018                var e = self.panel.trigger("buttons-detach", [self]);
12019                if (!e.isDefaultPrevented()) {
12020                    self.isAttached = !self.doDetach();
12021                }
12022                if (!self.isAttached) {
12023                    self.panel.trigger("buttons-detached", [self]);
12024                }
12025            }
12026            return !self.isAttached;
12027        },
12028        doDetach: function(){
12029            this.$el.detach();
12030            return true;
12031        },
12032
12033        beforeLoad: function(media){
12034            this.each(function(button){
12035                button.beforeLoad(media);
12036            });
12037        },
12038
12039        afterLoad: function(media){
12040            this.each(function(button){
12041                button.afterLoad(media);
12042            });
12043        },
12044
12045        close: function(){
12046            this.each(function(button){
12047                button.close();
12048            });
12049        },
12050
12051        resize: function(){
12052            const prev = this.get("prev");
12053            const next = this.get("next");
12054            const target = this.panel.isMobileLayout ? this.$el : this.panel.content.$buttons;
12055            if ( next ) next.appendTo( target );
12056            if ( prev ) prev.appendTo( target );
12057            this.each(function(button){
12058                button.resize();
12059            });
12060        }
12061    });
12062
12063})(
12064    FooGallery.$,
12065    FooGallery,
12066    FooGallery.icons,
12067    FooGallery.utils,
12068    FooGallery.utils.is
12069);
12070(function($, _, _icons, _utils, _is, _obj){
12071
12072    _.Panel.Button = _utils.Class.extend({
12073        construct: function(panel, name, options){
12074            this.panel = panel;
12075            this.name = name;
12076            this.opt = _obj.extend({
12077                icon: null,
12078                label: null,
12079                visible: !!panel.opt.buttons[name],
12080                disabled: false,
12081                toggle: false,
12082                pressed: false,
12083                group: null,
12084                click: $.noop,
12085                beforeLoad: $.noop,
12086                afterLoad: $.noop,
12087                close: $.noop,
12088                resize: $.noop
12089            }, options);
12090            this.cls = {
12091                elem: panel.cls.buttons[name],
12092                states: panel.cls.states
12093            };
12094            this.$el = null;
12095            this.groupName = this.opt.group;
12096            this.isVisible = this.opt.visible;
12097            this.isDisabled = this.opt.disabled;
12098            this.isToggle = this.opt.toggle;
12099            this.isPressed = this.opt.pressed;
12100            this.isCreated = false;
12101            this.isAttached = false;
12102        },
12103        isEnabled: function(){
12104            return this.panel.opt.buttons.hasOwnProperty(this.name) && this.panel.opt.buttons[this.name];
12105        },
12106        create: function(){
12107            var self = this;
12108            if (!self.isCreated){
12109                self.$el = $('<button/>', {
12110                    type: 'button',
12111                    "aria-label": self.opt.label,
12112                    "aria-disabled": self.isDisabled,
12113                    "aria-hidden": !self.isVisible
12114                }).addClass(self.cls.elem).on("click.foogallery", {self: self}, self.onButtonClick);
12115                if (_is.string(self.opt.icon)){
12116                    self.$el.append(_icons.get(self.opt.icon, self.panel.opt.icons));
12117                } else if (_is.array(self.opt.icon)){
12118                    self.opt.icon.forEach(function(icon){
12119                        self.$el.append(_icons.get(icon, self.panel.opt.icons));
12120                    });
12121                } else if (_is.fn(self.opt.icon)){
12122                    self.$el.append(self.opt.icon.call(this));
12123                }
12124                self.isCreated = true;
12125                var enabled = self.isEnabled();
12126                self.toggle(enabled);
12127                self.disable(!enabled);
12128                if (self.isToggle) self.press( self.isPressed );
12129            }
12130            return self.isCreated;
12131        },
12132        destroy: function(){
12133            if (this.isCreated){
12134                this.$el.off("click.foogallery").remove();
12135                this.isCreated = false;
12136            }
12137            return !this.isCreated;
12138        },
12139        appendTo: function(parent){
12140            if ( this.isAttached ) {
12141                this.detach();
12142            }
12143            if ((this.isCreated || this.create()) && !this.isAttached){
12144                this.$el.appendTo(parent);
12145                this.isAttached = true;
12146            }
12147            return this.isAttached;
12148        },
12149        prependTo: function(parent){
12150            if ( this.isAttached ) {
12151                this.detach();
12152            }
12153            if ((this.isCreated || this.create()) && !this.isAttached){
12154                this.$el.prependTo(parent);
12155                this.isAttached = true;
12156            }
12157            return this.isAttached;
12158        },
12159        detach: function(){
12160            if (this.isCreated && this.isAttached){
12161                this.$el.detach();
12162                this.isAttached = false;
12163            }
12164            return this.isAttached;
12165        },
12166        toggle: function(visible){
12167            if (!this.isCreated) return;
12168            this.isVisible = _is.boolean(visible) ? visible : !this.isVisible;
12169            this.$el.toggleClass(this.cls.states.hidden, !this.isVisible).attr("aria-hidden", !this.isVisible);
12170        },
12171        disable: function(disabled){
12172            if (!this.isCreated) return;
12173            this.isDisabled = _is.boolean(disabled) ? disabled : !this.isDisabled;
12174            this.$el.toggleClass(this.cls.states.disabled, this.isDisabled)
12175                .attr("aria-disabled", this.isDisabled)
12176                .prop("disabled", this.isDisabled);
12177        },
12178        press: function(pressed){
12179            if ( !this.isCreated ) return;
12180            this.isPressed = pressed;
12181            this.$el.attr("aria-pressed", this.isPressed);
12182        },
12183        beforeLoad: function(media){
12184            this.opt.beforeLoad.call(this, media);
12185        },
12186        afterLoad: function(media){
12187            this.opt.afterLoad.call(this, media);
12188        },
12189        close: function(){
12190            this.opt.close.call(this);
12191        },
12192        click: function(){
12193            this.opt.click.call(this);
12194        },
12195        resize: function(){
12196            this.opt.resize.call(this);
12197        },
12198        onButtonClick: function (e) {
12199            e.preventDefault();
12200            e.data.self.click();
12201        }
12202    });
12203
12204})(
12205    FooGallery.$,
12206    FooGallery,
12207    FooGallery.icons,
12208    FooGallery.utils,
12209    FooGallery.utils.is,
12210    FooGallery.utils.obj
12211);
12212(function($, _, _is){
12213
12214    _.Panel.SideAreaButton = _.Panel.Button.extend({
12215        construct: function(area){
12216            this._super(area.panel, area.name, {
12217                icon: area.opt.icon,
12218                label: area.opt.label,
12219                autoHideArea: area.opt.autoHide,
12220                click: area.toggle.bind(area),
12221                toggle: true,
12222                pressed: area.opt.visible,
12223                group: area.opt.group
12224            });
12225            this.area = area;
12226            this.__isVisible = null;
12227            this.__autoHide = null;
12228        },
12229        beforeLoad: function(media){
12230            var enabled = this.area.isEnabled(), supported = enabled && this.area.canLoad(media);
12231            if (!supported && this.__isVisible == null){
12232                this.__isVisible = this.area.isVisible;
12233                this.area.toggle(false);
12234            } else if (supported && _is.boolean(this.__isVisible)) {
12235                this.area.toggle(this.__isVisible);
12236                this.__isVisible = null;
12237            }
12238            if (enabled) this.disable(!supported);
12239            else this.toggle(supported);
12240
12241            this.checkAutoHide(enabled, supported);
12242
12243            this.opt.beforeLoad.call(this, media);
12244        },
12245        checkAutoHide: function(enabled, supported){
12246            if (enabled && supported && this.opt.autoHideArea === true){
12247                if (this.__autoHide == null && this.panel.isSmallScreen) {
12248                    this.__autoHide = this.area.isVisible;
12249                    this.area.toggle(false);
12250                    this.area.button.toggle(true);
12251                } else if (_is.boolean(this.__autoHide) && !this.panel.isSmallScreen) {
12252                    this.area.button.toggle(this.area.button.isEnabled() && this.area.opt.toggle);
12253                    this.area.toggle(this.__autoHide);
12254                    this.__autoHide = null;
12255                }
12256            }
12257        },
12258        isTargetingSamePosition: function( button ) {
12259            if ( button instanceof _.Panel.SideAreaButton ) {
12260                return button.area.isTargetingSamePosition( this.area );
12261            }
12262            return false;
12263        },
12264        resize: function(){
12265            var enabled = this.area.isEnabled(), supported = enabled && this.area.canLoad(this.area.currentMedia);
12266            this.checkAutoHide(enabled, supported);
12267        }
12268    });
12269
12270})(
12271    FooGallery.$,
12272    FooGallery,
12273    FooGallery.utils.is
12274);
12275(function($, _, _utils){
12276
12277    _.Panel.AutoProgress = _.Panel.Button.extend({
12278        construct: function(panel){
12279            var self = this;
12280            self.__stopped = !panel.opt.autoProgressStart;
12281            self.__timer = new _utils.Timer();
12282            self._super(panel, "autoProgress", {
12283                icon: "auto-progress",
12284                label: panel.il8n.buttons.autoProgress
12285            });
12286            self.$icon = null;
12287            self.$circle = null;
12288            self.circumference = 0;
12289        },
12290        isEnabled: function(){
12291            return this._super() && this.panel.opt.autoProgress > 0;
12292        },
12293        create: function () {
12294            if (this._super()){
12295                if (this.panel.opt.autoProgressVisible === false){
12296                    this.toggle(false);
12297                }
12298                this.$icon = this.$el.find("svg");
12299                this.$circle = this.$icon.find("circle");
12300                var radius = parseFloat(this.$circle.attr("r"));
12301                this.circumference = (radius * 2) * Math.PI;
12302                this.$circle.css({
12303                    "stroke-dasharray": this.circumference + ' ' + this.circumference,
12304                    "stroke-dashoffset": this.circumference + ''
12305                });
12306                this.__timer.on({
12307                    "start resume": this.onStartOrResume,
12308                    "pause": this.onPause,
12309                    "stop": this.onStop,
12310                    "tick": this.onTick,
12311                    "complete reset": this.onCompleteOrReset,
12312                    "complete": this.onComplete
12313                }, this);
12314            }
12315            return this.isCreated;
12316        },
12317        close: function(){
12318            this.__timer.pause();
12319            this._super();
12320        },
12321        destroy: function(){
12322            this.__timer.destroy();
12323            return this._super();
12324        },
12325        beforeLoad: function(media){
12326            if (this.isEnabled()) {
12327                this.__timer.reset();
12328            }
12329            this._super(media);
12330        },
12331        afterLoad: function(media){
12332            if (this.isEnabled()) {
12333                this.__timer.countdown(this.panel.opt.autoProgress);
12334                if (this.__stopped) this.__timer.pause();
12335            }
12336            this._super(media);
12337        },
12338        click: function(){
12339            if (this.__timer.isRunning){
12340                this.__stopped = true;
12341                this.__timer.pause();
12342            } else if (this.__timer.canResume) {
12343                this.__stopped = false;
12344                this.__timer.resume();
12345            } else {
12346                this.__stopped = false;
12347                this.__timer.restart();
12348            }
12349            this._super();
12350        },
12351        onStartOrResume: function(){
12352            this.$icon.removeClass(this.cls.states.allProgress).addClass(this.cls.states.started);
12353        },
12354        onPause: function(){
12355            this.$icon.removeClass(this.cls.states.allProgress).addClass(this.cls.states.paused);
12356        },
12357        onStop: function(){
12358            this.$icon.removeClass(this.cls.states.allProgress).addClass(this.cls.states.stopped);
12359        },
12360        onTick: function(e, current, time){
12361            var percent = current / time * 100;
12362            this.$circle.css("stroke-dashoffset", this.circumference - percent / 100 * this.circumference);
12363        },
12364        onCompleteOrReset: function(){
12365            this.$icon.removeClass(this.cls.states.allProgress);
12366        },
12367        onComplete: function(){
12368            this.panel.next();
12369        }
12370    });
12371
12372})(
12373    FooGallery.$,
12374    FooGallery,
12375    FooGallery.utils
12376);
12377
12378(function($, _, _utils){
12379
12380    _.fullscreen = new _utils.FullscreenAPI();
12381
12382    _.Panel.Fullscreen = _.Panel.Button.extend({
12383        construct: function(panel){
12384            var self = this;
12385            self._super(panel, "fullscreen", {
12386                icon: ["expand", "shrink"],
12387                label: panel.il8n.buttons.fullscreen,
12388                toggle: true
12389            });
12390        },
12391        isEnabled: function(){
12392            return this._super() && _.fullscreen.supported;
12393        },
12394        click: function(){
12395            var self = this, pnl = self.panel.el;
12396            _.fullscreen.toggle(pnl).then(function(){
12397                if (_.fullscreen.element() === pnl){
12398                    _.fullscreen.on("change error", self.onFullscreenChange, self);
12399                    self.enter();
12400                } else {
12401                    _.fullscreen.off("change error", self.onFullscreenChange, self);
12402                    self.exit();
12403                }
12404            }, function(err){
12405                console.debug('Error toggling fullscreen on element.', err, pnl);
12406            });
12407            self._super();
12408        },
12409        onFullscreenChange: function(){
12410            if (_.fullscreen.element() !== this.panel.el){
12411                this.exit();
12412            }
12413        },
12414        enter: function(){
12415            if (this.panel.isFullscreen) return;
12416            this.panel.isFullscreen = true;
12417            this.panel.$el.addClass(this.panel.cls.fullscreen);
12418            if (!this.panel.isMaximized){
12419                this.panel.$el.attr({
12420                    'role': 'dialog',
12421                    'aria-modal': true
12422                }).trigger('focus');
12423                this.panel.trapFocus();
12424            }
12425            this.panel.buttons.press('fullscreen', true);
12426            this.panel.buttons.toggle('maximize', false);
12427        },
12428        exit: function(){
12429            if (!this.panel.isFullscreen) return;
12430            this.panel.$el.removeClass(this.panel.cls.fullscreen);
12431            if (!this.panel.isMaximized){
12432                this.panel.$el.attr({
12433                    'role': null,
12434                    'aria-modal': null
12435                }).trigger('focus');
12436                this.panel.releaseFocus();
12437            }
12438            this.panel.buttons.press('fullscreen', false);
12439            this.panel.buttons.toggle('maximize', this.panel.isInline && this.panel.buttons.opt.maximize);
12440            this.panel.isFullscreen = false;
12441        }
12442    });
12443
12444})(
12445    FooGallery.$,
12446    FooGallery,
12447    FooGallery.utils
12448);
12449(function($, _, _is){
12450
12451    var root = document.documentElement;
12452    var scrollKeys = {
12453        " ": true,
12454        ArrowDown: true,
12455        ArrowLeft: true,
12456        ArrowRight: true,
12457        ArrowUp: true,
12458        End: true,
12459        Home: true,
12460        PageDown: true,
12461        PageUp: true
12462    };
12463
12464    function getElement( target ) {
12465        if (target && target.nodeType === 3) return target.parentElement;
12466        return target && target.nodeType === 1 ? target : null;
12467    }
12468
12469    function hasScrollableAncestor( target, panel ) {
12470        var element = getElement(target), style;
12471        while (element && panel.contains(element)) {
12472            style = window.getComputedStyle(element);
12473            if (
12474                (/(auto|scroll|overlay)/.test(style.overflowY) && element.scrollHeight > element.clientHeight) ||
12475                (/(auto|scroll|overlay)/.test(style.overflowX) && element.scrollWidth > element.clientWidth)
12476            ) {
12477                return true;
12478            }
12479            if (element === panel) break;
12480            element = element.parentElement;
12481        }
12482        return false;
12483    }
12484
12485    function isEditable( target ) {
12486        return target && (
12487            target.isContentEditable ||
12488            (target.tagName && /^(INPUT|TEXTAREA|SELECT|BUTTON|A)$/.test( target.tagName.toUpperCase() ))
12489        );
12490    }
12491
12492    _.Panel.Maximize = _.Panel.Button.extend({
12493        construct: function(panel){
12494            this._super(panel, "maximize", {
12495                icon: "maximize",
12496                label: panel.il8n.buttons.maximize,
12497                toggle: true
12498            });
12499            this._scrollState = null;
12500            this.$placeholder = $("<span/>");
12501        },
12502        click: function(){
12503            this.set(!this.panel.isMaximized);
12504            this._super();
12505        },
12506        close: function(){
12507            this.exit();
12508            this._super();
12509        },
12510        set: function(maximized, visible){
12511            if (maximized) this.enter();
12512            else this.exit();
12513            visible = _is.boolean(visible) ? visible : this.isVisible;
12514            this.toggle(visible);
12515        },
12516        lockScroll: function(){
12517            var body = document.body;
12518            if (!body || this._scrollState) return;
12519
12520            var pageHeight = Math.max(root.scrollHeight, body.scrollHeight, root.clientHeight);
12521            var panel = this.panel.$el.get(0);
12522            var state = this._scrollState = {
12523                x: window.scrollX,
12524                y: window.scrollY,
12525                rootMinHeight: root.style.minHeight,
12526                bodyPosition: body.style.position,
12527                bodyTop: body.style.top,
12528                bodyLeft: body.style.left,
12529                bodyRight: body.style.right,
12530                bodyWidth: body.style.width,
12531                preventScroll: function(event){
12532                    if (!hasScrollableAncestor(event.target, panel)) event.preventDefault();
12533                },
12534                preventKey: function(event){
12535                    if (
12536                        scrollKeys[event.key] &&
12537                        !isEditable(event.target) &&
12538                        !hasScrollableAncestor(event.target, panel)
12539                    ) {
12540                        event.preventDefault();
12541                    }
12542                }
12543            };
12544
12545            root.classList.add(this.panel.cls.noScrollbars);
12546            root.style.minHeight = pageHeight + "px";
12547            body.style.position = "fixed";
12548            body.style.top = -state.y + "px";
12549            body.style.left = -state.x + "px";
12550            body.style.right = "0";
12551            body.style.width = "100%";
12552
12553            document.addEventListener("wheel", state.preventScroll, {capture: true, passive: false});
12554            document.addEventListener("touchmove", state.preventScroll, {capture: true, passive: false});
12555            document.addEventListener("keydown", state.preventKey, true);
12556        },
12557        unlockScroll: function(){
12558            var state = this._scrollState, body = document.body;
12559            if (!state || !body) return;
12560
12561            document.removeEventListener("wheel", state.preventScroll, {capture: true});
12562            document.removeEventListener("touchmove", state.preventScroll, {capture: true});
12563            document.removeEventListener("keydown", state.preventKey, true);
12564
12565            root.classList.remove(this.panel.cls.noScrollbars);
12566            root.style.minHeight = state.rootMinHeight;
12567            body.style.position = state.bodyPosition;
12568            body.style.top = state.bodyTop;
12569            body.style.left = state.bodyLeft;
12570            body.style.right = state.bodyRight;
12571            body.style.width = state.bodyWidth;
12572
12573            this._scrollState = null;
12574            window.scrollTo(state.x, state.y);
12575        },
12576        enter: function(){
12577            if (this.panel.isMaximized) return;
12578            this.panel.isMaximized = true;
12579            this.$placeholder.insertAfter(this.panel.$el);
12580            this.panel.$el.appendTo("body").addClass(this.panel.cls.maximized).attr({
12581                'role': 'dialog',
12582                'aria-modal': true
12583            }).trigger('focus');
12584            this.panel.buttons.press('maximize', true);
12585            this.panel.trapFocus();
12586            if (this.panel.opt.noScrollbars){
12587                this.lockScroll();
12588            }
12589        },
12590        exit: function(){
12591            if (!this.panel.isMaximized) return;
12592            this.panel.isMaximized = false;
12593            this.panel.$el.removeClass(this.panel.cls.maximized).attr({
12594                'role': null,
12595                'aria-modal': null
12596            }).insertBefore(this.$placeholder);
12597            if (this.panel.isInline) this.panel.$el.trigger('focus');
12598            this.$placeholder.detach();
12599            this.panel.buttons.press('maximize', false);
12600            this.panel.releaseFocus();
12601            if (this.panel.opt.noScrollbars){
12602                this.unlockScroll();
12603            }
12604        }
12605    });
12606
12607})(
12608    FooGallery.$,
12609    FooGallery,
12610    FooGallery.utils.is
12611);
12612
12613(function($, _, _is){
12614
12615    _.Panel.Download = _.Panel.Button.extend({
12616        construct: function(panel){
12617            this._super(panel, "download", {
12618                icon: "download",
12619                label: panel.il8n.buttons.download,
12620                toggle: false
12621            });
12622            this.downloadable = [ 'image' ];
12623        },
12624        beforeLoad: function(media) {
12625            this._super( media );
12626            if ( this.isEnabled() ) {
12627                this.toggle( this.downloadable.includes( media?.item?.type ) );
12628            }
12629        },
12630        click: function(){
12631            this._super();
12632            const i = this.panel.currentItem;
12633            if ( i instanceof _.Item && _is.string( i.download ) ) {
12634                this.disable( true );
12635                _.downloadImage( i.download )
12636                    .catch( err => console.error( err ) )
12637                    .finally( () => this.disable( false ) );
12638            }
12639        }
12640    });
12641
12642})(
12643    FooGallery.$,
12644    FooGallery,
12645    FooGallery.utils.is
12646);
12647(function($, _, _utils, _is, _fn, _obj, _str){
12648
12649    /**
12650     * @memberof FooGallery.Panel.
12651     * @class Area
12652     * @augments FooGallery.utils.Class
12653     */
12654    _.Panel.Area = _utils.Class.extend(/** @lends FooGallery.Panel.Area */{
12655        /**
12656         * @ignore
12657         * @constructs
12658         * @param panel
12659         * @param name
12660         * @param options
12661         * @param classes
12662         */
12663        construct: function(panel, name, options, classes){
12664            this.panel = panel;
12665            this.name = name;
12666            this.opt = _obj.extend({
12667                waitForUnload: true
12668            }, options);
12669            this.cls = _obj.extend({
12670                elem: this.__cls(panel.cls.area.elem, name, true),
12671                inner: this.__cls(panel.cls.area.inner, name, true)
12672            }, classes);
12673            this.sel = _utils.selectify(this.cls);
12674            this.currentMedia = null;
12675            this.$el = null;
12676            this.$inner = null;
12677            this.isCreated = false;
12678            this.isAttached = false;
12679        },
12680        __cls: function(cls, replacement, andOriginal){
12681            var formatted = cls.replace(/-area($|-)/, "-" + replacement + "$1");
12682            return andOriginal ? [ cls, formatted ].join(" ") : formatted;
12683        },
12684        create: function(){
12685            var self = this;
12686            if (!self.isCreated) {
12687                var e = self.panel.trigger("area-create", [self]);
12688                if (!e.isDefaultPrevented()) {
12689                    self.isCreated = self.doCreate();
12690                }
12691                if (self.isCreated) {
12692                    self.panel.trigger("area-created", [self]);
12693                }
12694            }
12695            return self.isCreated;
12696        },
12697        doCreate: function(){
12698            this.$el = $("<div/>").addClass(this.cls.elem);
12699            this.$inner = $("<div/>").addClass(this.cls.inner).appendTo(this.$el);
12700            return true;
12701        },
12702        destroy: function(){
12703            var self = this;
12704            if (self.isCreated){
12705                var e = self.panel.trigger("area-destroy", [self]);
12706                if (!e.isDefaultPrevented()) {
12707                    self.isCreated = !self.doDestroy();
12708                }
12709                if (!self.isCreated) {
12710                    self.panel.trigger("area-destroyed", [self]);
12711                }
12712            }
12713            return !self.isCreated;
12714        },
12715        doDestroy: function(){
12716            if (this.currentMedia instanceof _.Panel.Media){
12717                this.currentMedia.detach();
12718            }
12719            this.$el.remove();
12720            return true;
12721        },
12722        appendTo: function( parent ){
12723            var self = this;
12724            if (!self.isCreated){
12725                self.create();
12726            }
12727            if (self.isCreated && !self.isAttached){
12728                var e = self.panel.trigger("area-append", [self, parent]);
12729                if (!e.isDefaultPrevented()) {
12730                    self.isAttached = self.doAppendTo( parent );
12731                }
12732                if (self.isAttached) {
12733                    self.panel.trigger("area-appended", [self, parent]);
12734                }
12735            }
12736            return self.isAttached;
12737        },
12738        doAppendTo: function( parent ){
12739            this.$el.appendTo( parent );
12740            return this.$el.parent().length > 0;
12741        },
12742        detach: function(){
12743            var self = this;
12744            if (self.isCreated && self.isAttached) {
12745                var e = self.panel.trigger("area-detach", [self]);
12746                if (!e.isDefaultPrevented()) {
12747                    self.isAttached = !self.doDetach();
12748                }
12749                if (!self.isAttached) {
12750                    self.panel.trigger("area-detached", [self]);
12751                }
12752            }
12753            return !self.isAttached;
12754        },
12755        doDetach: function(){
12756            this.$el.detach();
12757            return true;
12758        },
12759        load: function(media){
12760            var self = this;
12761            if (!(media instanceof _.Panel.Media)) return _fn.reject("unable to load media");
12762            return $.Deferred(function(def){
12763                var reverseTransition = self.shouldReverseTransition(self.currentMedia, media);
12764                var e = self.panel.trigger("area-load", [self, media, reverseTransition]);
12765                if (e.isDefaultPrevented()){
12766                    def.rejectWith("default prevented");
12767                    return;
12768                }
12769                var hasMedia = self.currentMedia instanceof _.Panel.Media, prev = self.currentMedia;
12770                if (self.opt.waitForUnload && hasMedia){
12771                    self.panel.trigger("area-unload", [self, prev]);
12772                    self.doUnload(prev, reverseTransition).then(function(){
12773                        self.panel.trigger("area-unloaded", [self, prev]);
12774                        self.currentMedia = media;
12775                        self.panel.trigger("area-load", [self, media]);
12776                        self.doLoad(media, reverseTransition).then(def.resolve).fail(def.reject);
12777                    }).fail(def.reject);
12778                } else {
12779                    if (hasMedia){
12780                        self.panel.trigger("area-unload", [self, prev]);
12781                        self.doUnload(prev, reverseTransition).then(function(){
12782                            self.panel.trigger("area-unloaded", [self, prev]);
12783                        });
12784                    }
12785                    self.currentMedia = media;
12786                    self.panel.trigger("area-load", [self, media]);
12787                    self.doLoad(media, reverseTransition).then(def.resolve).fail(def.reject);
12788                }
12789            }).then(function(){
12790                self.panel.trigger("area-loaded", [self, media]);
12791            }).fail(function(){
12792                self.panel.trigger("area-error", [self, media]);
12793            }).promise();
12794        },
12795        doLoad: function(media, reverseTransition){
12796            return _fn.resolved;
12797        },
12798        doUnload: function(media, reverseTransition){
12799            return _fn.resolved;
12800        },
12801        close: function(immediate){
12802            var self = this;
12803            if (self.currentMedia instanceof _.Panel.Media){
12804                var current = self.currentMedia;
12805                if (!immediate){
12806                    self.panel.trigger("area-unload", [self, current]);
12807                    return self.doUnload(current, false).then(function() {
12808                        self.panel.trigger("area-unloaded", [self, current]);
12809                    }).always(function(){
12810                        self.currentMedia = null;
12811                    });
12812                }
12813                self.panel.trigger("area-unload", [self, current]);
12814                self.doUnload(current, false).then(function(){
12815                    self.panel.trigger("area-unloaded", [self, current]);
12816                });
12817                self.currentMedia = null;
12818            }
12819            return _fn.resolved;
12820        },
12821        shouldReverseTransition: function( oldMedia, newMedia ){
12822            if (!(oldMedia instanceof _.Panel.Media) || !(newMedia instanceof _.Panel.Media)) return true;
12823            var result = oldMedia.item.index < newMedia.item.index,
12824                last = this.panel.tmpl.items.last(this.panel.isVisible);
12825            if (last instanceof _.Item && ((newMedia.item.index === 0 && oldMedia.item.index === last.index) || (newMedia.item.index === last.index && oldMedia.item.index === 0))){
12826                result = !result;
12827            }
12828            return result;
12829        },
12830        listen: function(){},
12831        stopListening: function(){},
12832        resize: function(){}
12833    });
12834
12835})(
12836    FooGallery.$,
12837    FooGallery,
12838    FooGallery.utils,
12839    FooGallery.utils.is,
12840    FooGallery.utils.fn,
12841    FooGallery.utils.obj,
12842    FooGallery.utils.str
12843);
12844
12845(function($, _, _fn, _t){
12846
12847    /**
12848     * @memberof FooGallery.Panel.
12849     * @class Content
12850     * @augments FooGallery.Panel.Area
12851     */
12852    _.Panel.Content = _.Panel.Area.extend(/** @lends FooGallery.Panel.Content */{
12853        /**
12854         * @ignore
12855         * @constructs
12856         * @param panel
12857         */
12858        construct: function(panel){
12859            this._super(panel, "content", {
12860                waitForUnload: false
12861            }, panel.cls.content);
12862            this.$buttons = null;
12863            this.robserver = null;
12864        },
12865        doCreate: function(){
12866            var self = this;
12867            if (self._super()){
12868                self.$buttons = $( "<div/>" ).addClass( self.cls.buttons ).appendTo( self.$el );
12869                if (self.panel.opt.swipe){
12870                    self.$inner.fgswipe({data: {self: self}, swipe: self.onSwipe, allowPageScroll: true});
12871                }
12872                self.robserver = new ResizeObserver(_fn.throttle(function () {
12873                    if (self.panel instanceof _.Panel && !self.panel.destroying && !self.panel.destroyed) {
12874                        // only the inner is being observed so if a change occurs we can safely just call resize
12875                        self.resize();
12876                    }
12877                }, 50));
12878                self.robserver.observe(self.$inner.get(0));
12879                return true;
12880            }
12881            return false;
12882        },
12883        doDestroy: function(){
12884            if (this.robserver instanceof ResizeObserver){
12885                this.robserver.disconnect();
12886            }
12887            this.$inner.fgswipe("destroy");
12888            return this._super();
12889        },
12890        doTransition: function($element, trigger){
12891            var element = $element.get(0);
12892            if (!element || typeof element.getAnimations !== "function"){
12893                return _t.start($element, trigger, null, 350);
12894            }
12895            return $.Deferred(function(def){
12896                requestAnimationFrame(function(){
12897                    trigger($element);
12898                    var animations = element.getAnimations().filter(function(animation){
12899                        var timing = animation.effect && typeof animation.effect.getComputedTiming === "function"
12900                            ? animation.effect.getComputedTiming()
12901                            : null;
12902                        return animation.playState !== "paused"
12903                            && timing !== null
12904                            && typeof timing.endTime === "number"
12905                            && isFinite(timing.endTime);
12906                    });
12907                    if (animations.length === 0){
12908                        def.resolve();
12909                        return;
12910                    }
12911                    Promise.all(animations.map(function(animation){
12912                        return animation.finished;
12913                    })).then(def.resolve, def.reject);
12914                });
12915            }).promise();
12916        },
12917        doLoad: function(media, reverseTransition){
12918            var self = this, states = self.panel.cls.states;
12919            return $.Deferred(function (def) {
12920                if (!media.isCreated) media.create();
12921                media.$el.toggleClass(states.reverse, reverseTransition);
12922                media.appendTo(self.$inner);
12923                var wait = [];
12924                if (self.panel.hasTransition){
12925                    wait.push(self.doTransition(media.$el, function($el){
12926                        $el.addClass(states.visible);
12927                    }));
12928                } else {
12929                    media.$el.addClass(states.visible);
12930                }
12931                wait.push(media.load());
12932                $.when.apply($, wait).then(def.resolve).fail(def.reject);
12933            }).promise();
12934        },
12935        doUnload: function(media, reverseTransition){
12936            var self = this, states = self.panel.cls.states;
12937            return $.Deferred(function (def) {
12938                var wait = [];
12939                if (media.isCreated){
12940                    media.$el.toggleClass(states.reverse, !reverseTransition);
12941                    if (self.panel.hasTransition){
12942                        wait.push(self.doTransition(media.$el, function($el){
12943                            $el.removeClass(states.visible);
12944                        }));
12945                    } else {
12946                        media.$el.removeClass(states.visible);
12947                    }
12948                }
12949                wait.push(media.unload());
12950                $.when.apply($, wait).then(def.resolve).fail(def.reject);
12951            }).always(function(){
12952                if (media.isCreated){
12953                    media.$el.removeClass(states.reverse);
12954                }
12955                media.detach();
12956            }).promise();
12957        },
12958        onSwipe: function(info, data){
12959            var self = data.self;
12960            if (info.direction === "E"){
12961                self.panel.prev();
12962            }
12963            if (info.direction === "W"){
12964                self.panel.next();
12965            }
12966        },
12967        resize: function(){
12968            if (this.currentMedia instanceof _.Panel.Media){
12969                this.currentMedia.resize();
12970            }
12971        }
12972    });
12973
12974})(
12975    FooGallery.$,
12976    FooGallery,
12977    FooGallery.utils.fn,
12978    FooGallery.utils.transition
12979);
12980
12981(function($, _, _icons, _utils, _is, _fn, _obj){
12982
12983    /**
12984     * @memberof FooGallery.Panel.
12985     * @class Area
12986     * @augments FooGallery.Panel.SideArea
12987     */
12988    _.Panel.SideArea = _.Panel.Area.extend(/** @lends FooGallery.Panel.SideArea */{
12989        /**
12990         * @ignore
12991         * @constructs
12992         * @param panel
12993         * @param name
12994         * @param options
12995         * @param classes
12996         */
12997        construct: function(panel, name, options, classes){
12998            var self = this, cls = panel.cls.sideArea;
12999            self._super(panel, name, _obj.extend({
13000                icon: null,
13001                label: null,
13002                position: null,
13003                overlay: false,
13004                visible: true,
13005                autoHide: false,
13006                toggle: !!panel.opt.buttons[name],
13007                priority: 99
13008            }, options), _obj.extend({
13009                toggle: this.__cls(cls.toggle, name, true),
13010                visible: this.__cls(cls.visible, name),
13011                overlay: this.__cls(cls.overlay, name),
13012                position: {
13013                    top: this.__cls(cls.position.top, name),
13014                    right: this.__cls(cls.position.right, name),
13015                    bottom: this.__cls(cls.position.bottom, name),
13016                    left: this.__cls(cls.position.left, name),
13017                }
13018            }, classes));
13019            self.isVisible = self.opt.visible;
13020            self.allPositionClasses = Object.keys(self.cls.position).map(function (key) {
13021                return self.cls.position[key];
13022            }).join(" ") + " " + self.cls.overlay;
13023            self.button = self.registerButton();
13024        },
13025        registerButton: function(){
13026            var btn = new _.Panel.SideAreaButton(this);
13027            this.panel.buttons.register(btn, this.opt.priority);
13028            return btn;
13029        },
13030        doCreate: function(){
13031            if (this._super()){
13032                if (this.isEnabled()){
13033                    this.panel.$el.toggleClass(this.cls.visible, this.isVisible);
13034                    this.setPosition( this.opt.position );
13035                }
13036                return true;
13037            }
13038            return false;
13039        },
13040        isEnabled: function(){
13041            return this.cls.position.hasOwnProperty(this.opt.position);
13042        },
13043        canLoad: function(media){
13044            return this.isEnabled() && media instanceof _.Panel.Media;
13045        },
13046        getPosition: function(){
13047            if (this.isEnabled()){
13048                const position = this.cls.position[this.opt.position];
13049                return this.opt.overlay ? position + " " + this.cls.overlay : position;
13050            }
13051            return null;
13052        },
13053        /**
13054         *
13055         * @param {string} position
13056         * @param {?boolean} [overlay]
13057         */
13058        setPosition: function( position, overlay = null ){
13059            this.opt.position = this.cls.position.hasOwnProperty(position) ? position : null;
13060            if (_is.boolean(overlay)) this.opt.overlay = overlay;
13061            if (_is.jq(this.panel.$el)){
13062                this.panel.$el.removeClass(this.allPositionClasses).addClass(this.getPosition());
13063            }
13064        },
13065        toggle: function( visible ){
13066            this.isVisible = _is.boolean(visible) ? visible : !this.isVisible;
13067            if (_is.jq(this.panel.$el)) {
13068                this.panel.$el.toggleClass(this.cls.visible, this.isVisible);
13069            }
13070            this.panel.buttons.press( this.name, this.isVisible );
13071        },
13072        onToggleClick: function(e){
13073            e.preventDefault();
13074            e.data.self.toggle();
13075        },
13076        isTargetingSamePosition: function( area ) {
13077            if ( area instanceof _.Panel.SideArea ) {
13078                const pos1 = this.opt.position,
13079                    pos2 = area.opt.position;
13080                return _is.string( pos1 ) && _is.string( pos2 ) && pos1 === pos2;
13081            }
13082            return false;
13083        }
13084    });
13085
13086})(
13087    FooGallery.$,
13088    FooGallery,
13089    FooGallery.icons,
13090    FooGallery.utils,
13091    FooGallery.utils.is,
13092    FooGallery.utils.fn,
13093    FooGallery.utils.obj
13094);
13095(function($, _, _is, _fn){
13096
13097    /**
13098     * @memberof FooGallery.Panel.
13099     * @class Info
13100     * @augments FooGallery.Panel.SideArea
13101     */
13102    _.Panel.Info = _.Panel.SideArea.extend(/** @lends FooGallery.Panel.Info */{
13103        /**
13104         * @ignore
13105         * @constructs
13106         * @param panel
13107         */
13108        construct: function(panel){
13109            this._super(panel, "info", {
13110                icon: "info",
13111                label: panel.il8n.buttons.info,
13112                position: panel.opt.info,
13113                overlay: panel.opt.infoOverlay,
13114                visible: panel.opt.infoVisible,
13115                autoHide: panel.opt.infoAutoHide,
13116                align: panel.opt.infoAlign,
13117                waitForUnload: false,
13118                group: "overlay",
13119                priority: 90
13120            }, panel.cls.info);
13121        },
13122        doCreate: function(){
13123            var self = this;
13124            if (self.isEnabled() && self._super()) {
13125                if (_is.string(self.opt.align) && self.cls.align.hasOwnProperty(self.opt.align)){
13126                    self.panel.$el.addClass(self.cls.align[self.opt.align]);
13127                }
13128                return true;
13129            }
13130            return false;
13131        },
13132        canLoad: function(media){
13133            return this._super(media) && media.caption.canLoad();
13134        },
13135        doLoad: function(media, reverseTransition){
13136            if (this.canLoad(media)){
13137                media.caption.appendTo(this.$inner);
13138                media.caption.load();
13139            }
13140            return _fn.resolved;
13141        },
13142        doUnload: function(media, reverseTransition){
13143            media.caption.unload();
13144            media.caption.detach();
13145            return _fn.resolved;
13146        }
13147    });
13148
13149})(
13150    FooGallery.$,
13151    FooGallery,
13152    FooGallery.utils.is,
13153    FooGallery.utils.fn
13154);
13155(function($, _, _icons, _utils, _is, _fn, _t){
13156
13157    /**
13158     * @memberof FooGallery.Panel.
13159     * @class Thumbs
13160     * @augments FooGallery.Panel.SideArea
13161     */
13162    _.Panel.Thumbs = _.Panel.SideArea.extend(/** @lends FooGallery.Panel.Thumbs */{
13163        /**
13164         * @ignore
13165         * @constructs
13166         * @param panel
13167         */
13168        construct: function(panel){
13169            this._super(panel, "thumbs", {
13170                icon: "thumbs",
13171                label: panel.il8n.buttons.thumbs,
13172                position: panel.opt.thumbs,
13173                captions: panel.opt.thumbsCaptions,
13174                align: panel.opt.thumbsCaptionsAlign,
13175                small: panel.opt.thumbsSmall,
13176                bestFit: panel.opt.thumbsBestFit,
13177                toggle: false,
13178                waitForUnload: false
13179            }, panel.cls.thumbs);
13180
13181            this.iobserver = null;
13182            this.robserver = null;
13183            this.$prev = null;
13184            this.$next = null;
13185            this.$viewport = null;
13186            this.$stage = null;
13187            this.$dummy = null;
13188
13189            this.__items = [];
13190            this.__animationFrameId = null;
13191
13192            this.info = this.getInfo();
13193            this.selectedIndex = 0;
13194            this.scrollIndex = 0;
13195            this.lastIndex = 0;
13196        },
13197        isHorizontal: function(){
13198            return ["top","bottom"].indexOf(this.opt.position) !== -1;
13199        },
13200        doCreate: function(){
13201            var self = this;
13202            if (self.isEnabled() && self._super()){
13203                if (!self.opt.captions) self.panel.$el.addClass(self.cls.noCaptions);
13204                if (self.opt.small) self.panel.$el.addClass(self.cls.small);
13205                if (_is.string(self.opt.align) && self.cls.align.hasOwnProperty(self.opt.align)) self.panel.$el.addClass(self.cls.align[self.opt.align]);
13206
13207                self.$prev = $('<button/>', {type: 'button'}).addClass(self.cls.prev)
13208                    .append(_icons.get("arrow-left", self.panel.opt.icons))
13209                    .on("click.foogallery", {self: self}, self.onPrevClick)
13210                    .prependTo(self.$inner);
13211                self.$viewport = $('<div/>').addClass(self.cls.viewport).appendTo(self.$inner);
13212                self.$next = $('<button/>', {type: 'button'}).addClass(self.cls.next)
13213                    .append(_icons.get("arrow-right", self.panel.opt.icons))
13214                    .on("click.foogallery", {self: self}, self.onNextClick)
13215                    .appendTo(self.$inner);
13216                self.$stage = $('<div/>').addClass(self.cls.stage).appendTo(self.$viewport);
13217                self.$dummy = $('<div/>').addClass(self.cls.thumb.elem).appendTo(self.$viewport);
13218
13219                self.iobserver = new IntersectionObserver(function(entries){
13220                    entries.forEach(function(entry){
13221                        if (entry.isIntersecting){
13222                            self.iobserver.unobserve(entry.target);
13223                            self.loadThumbElement(entry.target);
13224                        }
13225                    });
13226                }, { root: self.$inner.get(0), rootMargin: "82px 300px" });
13227
13228                self.robserver = new ResizeObserver(_fn.throttle(function (entries) {
13229                    if (entries.length > 0 && self.panel instanceof _.Panel && !self.panel.destroying && !self.panel.destroyed) {
13230                        // only the viewport is being observed so if a change occurs we can safely grab just the first entry
13231                        var size = _utils.getResizeObserverSize(entries[0]), viewport = self.info.viewport;
13232                        var diffX = Math.floor(Math.abs(size.width - viewport.width)),
13233                            diffY = Math.floor(Math.abs(size.height - viewport.height));
13234                        if (self.isVisible && (diffX > 1 || diffY > 1)) {
13235                            self.resize();
13236                        }
13237                    }
13238                }, 50));
13239
13240                self.doCreateThumbs(self.panel.tmpl.items.available(self.panel.isVisible));
13241
13242                return true;
13243            }
13244            return false;
13245        },
13246        doCreateThumbs: function(items){
13247            if (_is.empty(items)) return;
13248            var self = this;
13249            if (self.iobserver instanceof IntersectionObserver){
13250                self.iobserver.takeRecords().forEach(function(entry){
13251                    self.iobserver.unobserve(entry.target);
13252                });
13253            }
13254            self.__items = items;
13255            self.selectedIndex = 0;
13256            self.scrollIndex = 0;
13257            self.lastIndex = self.__items.length - 1;
13258            self.$stage.empty();
13259            items.forEach(function(item){
13260                var $thumb = self.doCreateThumb(item).appendTo(self.$stage);
13261                self.iobserver.observe($thumb.get(0));
13262            });
13263            self.$stage.append($("<div/>").addClass(self.cls.spacer));
13264        },
13265        doCreateThumb: function(item){
13266            var self = this, cls = self.cls.thumb;
13267            return $("<figure/>").addClass(cls.elem).addClass(item.getTypeClass()).addClass(self.panel.cls.states.idle).append(
13268                $("<div/>").addClass(cls.media).append(
13269                    $("<div/>").addClass(cls.overlay),
13270                    $("<div/>").addClass(cls.wrap).append(
13271                        $("<img/>", {title: item.title, alt: item.alt}).attr({draggable: false}).addClass(cls.image)
13272                    ),
13273                    $("<div/>").addClass(self.panel.cls.loader)
13274                ),
13275                $("<div/>").addClass(cls.caption).append(
13276                    $("<div/>").addClass(cls.title).html(item.caption),
13277                    $("<div/>").addClass(cls.description).html(item.description)
13278                )
13279            ).data("item", item).on("click", {self: self, item: item}, self.onThumbClick);
13280        },
13281        doDestroy: function(){
13282            this.stopListening();
13283            if (this.iobserver instanceof IntersectionObserver){
13284                this.iobserver.disconnect();
13285            }
13286            if (this.robserver instanceof ResizeObserver){
13287                this.robserver.disconnect();
13288            }
13289            return this._super();
13290        },
13291        doLoad: function(media, reverseTransition){
13292            if (this.isCreated){
13293                var index = this.__items.indexOf(media.item);
13294                if (index !== -1) {
13295                    this.makeVisible(index);
13296                    this.$stage.find(this.sel.thumb.elem)
13297                        .removeClass(this.panel.cls.states.selected)
13298                        .eq(index).addClass(this.panel.cls.states.selected);
13299                    this.selectedIndex = index;
13300                }
13301            }
13302            return _fn.resolved;
13303        },
13304        makeVisible: function(index, disableTransition){
13305            if (index <= this.scrollIndex) {
13306                this.goto(index, disableTransition);
13307            } else if (index >= this.scrollIndex + this.info.count) {
13308                this.goto(index, disableTransition);
13309            }
13310        },
13311        listen: function(){
13312            var self = this;
13313            self.stopListening();
13314            if (self.isCreated){
13315                self.resize();
13316                self.robserver.observe(self.$viewport.get(0));
13317                self.$inner.fgswipe({data: {self: self}, swipe: self.onSwipe, allowPageScroll: true})
13318                    .on("DOMMouseScroll.foogallery-panel-thumbs mousewheel.foogallery-panel-thumbs", {self: self}, self.onMouseWheel);
13319            }
13320        },
13321        stopListening: function(){
13322            if (this.isCreated){
13323                this.$inner.fgswipe("destroy").off(".foogallery-panel-thumbs");
13324                this.$stage.find(this.sel.thumb).css({width: "", minWidth: "", height: "", minHeight: ""});
13325                this.robserver.unobserve(this.$viewport.get(0));
13326            }
13327        },
13328        loadThumbElement: function(element){
13329            var self = this,
13330                $thumb = $(element),
13331                item = $thumb.data("item"),
13332                img = $thumb.find(self.sel.thumb.image).get(0),
13333                states = self.panel.cls.states;
13334
13335            $thumb.removeClass(states.allLoading).addClass(states.loading);
13336            img.onload = function(){
13337                img.onload = img.onerror = null;
13338                $thumb.removeClass(states.allLoading).addClass(states.loaded);
13339            };
13340            img.onerror = function(){
13341                img.onload = img.onerror = null;
13342                $thumb.removeClass(states.allLoading).addClass(states.error);
13343            };
13344            img.src = item.src;
13345            img.srcset = item.srcset;
13346            if (img.complete){
13347                img.onload();
13348            }
13349        },
13350        goto: function(index, disableTransition){
13351            var self = this;
13352            if (!self.isCreated) return _fn.reject("thumbs not created");
13353
13354            index = index < 0 ? 0 : (index > self.lastIndex ? self.lastIndex : index);
13355
13356            var states = self.panel.cls.states,
13357                rightOrBottom = index >= self.scrollIndex + self.info.count, // position the thumb to the right or bottom of the viewport depending on orientation
13358                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
13359                maxIndex = self.lastIndex - (self.info.count - 1); // the scrollIndex of the last item
13360
13361            // fix any calculated value overflows
13362            if (scrollIndex < 0) scrollIndex = 0;
13363            if (maxIndex < 0) maxIndex = 0;
13364            if (scrollIndex > maxIndex) scrollIndex = maxIndex;
13365
13366            return $.Deferred(function(def){
13367                // find the thumb
13368                var $thumb = self.$stage.find(self.sel.thumb.elem).eq(scrollIndex);
13369                if ($thumb.length > 0){
13370                    // align the right or bottom edge of the thumb with the viewport
13371                    var alignRightOrBottom = scrollIndex > self.scrollIndex, hasFullStage = self.__items.length >= self.info.count, offset, translate;
13372                    if (self.info.isHorizontal) {
13373                        offset = -($thumb.prop("offsetLeft"));
13374                        if (alignRightOrBottom) offset += self.info.remaining.width;
13375                        if (hasFullStage && self.info.stage.width - Math.abs(offset) < self.info.viewport.width) {
13376                            offset = self.info.viewport.width - self.info.stage.width;
13377                        }
13378                        translate = "translateX(" + (offset - 1) + "px)";
13379                    } else {
13380                        offset = -($thumb.prop("offsetTop"));
13381                        if (alignRightOrBottom) offset += self.info.remaining.height;
13382                        if (hasFullStage && self.info.stage.height - Math.abs(offset) < self.info.viewport.height) {
13383                            offset = self.info.viewport.height - self.info.stage.height;
13384                        }
13385                        translate = "translateY(" + (offset - 1) + "px)";
13386                    }
13387                    if (self.panel.hasTransition && !disableTransition) {
13388                        _t.start(self.$stage, function ($el) {
13389                            $el.css("transform", translate);
13390                        }, null, 350).then(function () {
13391                            def.resolve();
13392                        }).fail(def.reject);
13393                    } else {
13394                        self.$stage.addClass(states.noTransitions).css("transform", translate);
13395                        self.$stage.prop("offsetHeight");
13396                        self.$stage.removeClass(states.noTransitions);
13397                        def.resolve();
13398                    }
13399                } else {
13400                    def.resolve();
13401                }
13402            }).always(function(){
13403                self.scrollIndex = scrollIndex;
13404                self.disableButton(self.$prev, scrollIndex <= 0, states.disabled);
13405                self.disableButton(self.$next, scrollIndex >= maxIndex, states.disabled);
13406            }).promise();
13407        },
13408        disableButton: function( $button, state, cls ) {
13409            $button.toggleClass( cls, state )
13410                .attr("aria-disabled", state)
13411                .prop("disabled", state);
13412        },
13413        getInfo: function(){
13414            var isHorizontal = this.isHorizontal(),
13415                viewport = { width: 0, height: 0 },
13416                stage = { width: 0, height: 0 },
13417                original = { width: 0, height: 0 },
13418                counts = { horizontal: 0, vertical: 0 },
13419                adjusted = { width: 0, height: 0 },
13420                remaining = { width: 0, height: 0 },
13421                width = 0, height = 0, itemCount = this.__items.length;
13422            if (this.isCreated){
13423                viewport = { width: this.$viewport.innerWidth() + 1, height: this.$viewport.innerHeight() + 1 };
13424                original = { width: this.$dummy.outerWidth(), height: this.$dummy.outerHeight() };
13425                counts = { horizontal: Math.floor(viewport.width / original.width), vertical: Math.floor(viewport.height / original.height) };
13426                adjusted = { width: viewport.width / Math.min(itemCount, counts.horizontal), height: viewport.height / Math.min(itemCount, counts.vertical) };
13427                width = this.opt.bestFit ? adjusted.width : original.width;
13428                height = this.opt.bestFit ? adjusted.height : original.height;
13429                stage = { width: isHorizontal ? this.__items.length * width : width, height: !isHorizontal ? this.__items.length * height : height };
13430                remaining = { width: Math.floor(viewport.width - (counts.horizontal * width)), height: Math.floor(viewport.height - (counts.vertical * height)) };
13431            }
13432            return {
13433                isHorizontal: isHorizontal,
13434                viewport: viewport,
13435                stage: stage,
13436                original: original,
13437                adjusted: adjusted,
13438                remaining: remaining,
13439                counts: counts,
13440                count: isHorizontal ? counts.horizontal : counts.vertical,
13441                width: width,
13442                height: height
13443            };
13444        },
13445        resize: function(){
13446            if (this.isCreated){
13447                this.info = this.getInfo();
13448                if (this.opt.bestFit){
13449                    if (this.info.isHorizontal){
13450                        this.$stage.find(this.sel.thumb.elem).css({width: this.info.width, minWidth: this.info.width, height: "", minHeight: ""});
13451                    } else {
13452                        this.$stage.find(this.sel.thumb.elem).css({height: this.info.height, minHeight: this.info.height, width: "", minWidth: ""});
13453                    }
13454                }
13455                this.goto(this.scrollIndex, true);
13456            }
13457        },
13458        onThumbClick: function(e){
13459            e.preventDefault();
13460            e.data.self.panel.load(e.data.item);
13461        },
13462        onPrevClick: function(e){
13463            e.preventDefault();
13464            var self = e.data.self;
13465            self.goto(self.scrollIndex - (self.info.count - 1 || 1));
13466        },
13467        onNextClick: function(e){
13468            e.preventDefault();
13469            var self = e.data.self;
13470            self.goto(self.scrollIndex + (self.info.count - 1 || 1));
13471        },
13472        onSwipe: function(info, data){
13473            var self = data.self, amount = 1;
13474            if (self.info.isHorizontal){
13475                amount = Math.ceil(info.distance / self.info.width);
13476                if (info.direction === "E"){
13477                    self.goto(self.scrollIndex - amount);
13478                }
13479                if (info.direction === "W"){
13480                    self.goto(self.scrollIndex + amount);
13481                }
13482            } else {
13483                amount = Math.ceil(info.distance / self.info.height);
13484                if (info.direction === "S"){
13485                    self.goto(self.scrollIndex - amount);
13486                }
13487                if (info.direction === "N"){
13488                    self.goto(self.scrollIndex + amount);
13489                }
13490            }
13491        },
13492        onMouseWheel: function(e){
13493            var self = e.data.self,
13494                delta = Math.max(-1, Math.min(1, (e.originalEvent.wheelDelta || -e.originalEvent.detail)));
13495            if (delta > 0){
13496                self.goto(self.scrollIndex - 1);
13497                e.preventDefault();
13498            } else if (delta < 0){
13499                self.goto(self.scrollIndex + 1);
13500                e.preventDefault();
13501            }
13502        }
13503    });
13504
13505})(
13506    FooGallery.$,
13507    FooGallery,
13508    FooGallery.icons,
13509    FooGallery.utils,
13510    FooGallery.utils.is,
13511    FooGallery.utils.fn,
13512    FooGallery.utils.transition
13513);
13514
13515(function($, _, _fn, _t){
13516
13517    _.Panel.Cart = _.Panel.SideArea.extend({
13518        construct: function(panel){
13519            this._super(panel, "cart", {
13520                icon: "cart",
13521                label: panel.il8n.buttons.cart,
13522                position: panel.opt.cart,
13523                overlay: panel.opt.cartOverlay,
13524                visible: panel.opt.cartVisible,
13525                autoHide: panel.opt.cartAutoHide,
13526                waitForUnload: false,
13527                group: "overlay"
13528            }, panel.cls.cart);
13529        },
13530        canLoad: function(media){
13531            return this._super(media) && media.product.canLoad();
13532        },
13533        doLoad: function(media, reverseTransition){
13534            if (this.canLoad(media)){
13535                media.product.appendTo(this.$inner);
13536                media.product.load();
13537            }
13538            return _fn.resolved;
13539        },
13540        doUnload: function(media, reverseTransition){
13541            media.product.unload();
13542            media.product.detach();
13543            return _fn.resolved;
13544        }
13545    });
13546
13547})(
13548    FooGallery.$,
13549    FooGallery,
13550    FooGallery.utils.fn,
13551    FooGallery.utils.transition
13552);
13553(function($, _, _utils, _is, _fn, _obj, _str, _t){
13554
13555    _.Panel.Media = _utils.Class.extend({
13556        construct: function(panel, item){
13557            var self = this;
13558
13559            self.panel = panel;
13560
13561            self.item = item;
13562
13563            self.opt = _obj.extend({}, panel.opt.media);
13564
13565            self.cls = _obj.extend({}, panel.cls.media);
13566
13567            self.sel = _obj.extend({}, panel.sel.media);
13568
13569            self.il8n = _obj.extend({}, panel.il8n.media);
13570
13571            self.caption = new _.Panel.Media.Caption(panel, self);
13572
13573            if ( _.Panel.Media.Product ){
13574                self.product = new _.Panel.Media.Product(panel, self);
13575            }
13576
13577            if ( _.Panel.Media.Comments ){
13578                self.comments = new _.Panel.Media.Comments(panel, self);
13579            }
13580
13581            self.$el = null;
13582
13583            self.$content = null;
13584
13585            self.isCreated = false;
13586
13587            self.isAttached = false;
13588
13589            self.isLoading = false;
13590
13591            self.isLoaded = false;
13592
13593            self.isError = false;
13594        },
13595        getSize: function(attrWidth, attrHeight, defWidth, defHeight){
13596            var self = this, size = {};
13597            if (!_is.string(attrWidth) || !_is.string(attrHeight)) return size;
13598
13599            size[attrWidth] = _is.size(defWidth) ? defWidth : null;
13600            size[attrHeight] = _is.size(defHeight) ? defHeight : null;
13601
13602
13603            if (!self.item.isCreated) return size;
13604
13605            size[attrWidth] = self.item.$anchor.data(attrWidth) || size[attrWidth];
13606            size[attrHeight] = self.item.$anchor.data(attrHeight) || size[attrHeight];
13607            return size;
13608        },
13609        getSizes: function(){
13610            var self = this,
13611                size = self.getSize("width", "height", self.opt.width, self.opt.height),
13612                max = self.getSize("maxWidth", "maxHeight", self.opt.maxWidth, self.opt.maxHeight),
13613                min = self.getSize("minWidth", "minHeight", self.opt.minWidth, self.opt.minHeight);
13614            return _obj.extend(size, max, min);
13615        },
13616        destroy: function(){
13617            var self = this;
13618            var e = self.panel.trigger("media-destroy", [self]);
13619            if (!e.isDefaultPrevented()) {
13620                self.isCreated = !self.doDestroy();
13621            }
13622            if (!self.isCreated) {
13623                self.panel.trigger("media-destroyed", [self]);
13624            }
13625            return !self.isCreated;
13626        },
13627        doDestroy: function(){
13628            var self = this;
13629            if (self.isCreated){
13630                self.caption.destroy();
13631                self.detach();
13632                self.$el.remove();
13633            }
13634            return true;
13635        },
13636        create: function(){
13637            var self = this;
13638            if (!self.isCreated && _is.string(self.item.href)) {
13639                var e = self.panel.trigger("media-create", [self]);
13640                if (!e.isDefaultPrevented()) {
13641                    self.isCreated = self.doCreate();
13642                }
13643                if (self.isCreated) {
13644                    self.panel.trigger("media-created", [self]);
13645                }
13646            }
13647            return self.isCreated;
13648        },
13649        doCreate: function(){
13650            var self = this;
13651            self.$el = $('<div/>').addClass([self.cls.elem, self.cls.type].join(" ")).append(
13652                $('<div/>').addClass(self.panel.cls.loader)
13653            );
13654            self.$content = self.doCreateContent().addClass(self.cls.content).css(self.getSizes()).appendTo(self.$el);
13655            return true;
13656        },
13657        doCreateContent: function(){
13658            return $();
13659        },
13660        appendTo: function( parent ){
13661            var self = this;
13662            if (!self.isCreated){
13663                self.create();
13664            }
13665            if (self.isCreated && !self.isAttached){
13666                var e = self.panel.trigger("media-append", [self, parent]);
13667                if (!e.isDefaultPrevented()) {
13668                    self.isAttached = self.doAppendTo( parent );
13669                }
13670                if (self.isAttached) {
13671                    self.panel.trigger("media-appended", [self, parent]);
13672                }
13673            }
13674            return self.isAttached;
13675        },
13676        doAppendTo: function( parent ){
13677            this.$el.appendTo( parent );
13678            return this.$el.parent().length > 0;
13679        },
13680        detach: function(){
13681            var self = this;
13682            if (self.isCreated && self.isAttached) {
13683                var e = self.panel.trigger("media-detach", [self]);
13684                if (!e.isDefaultPrevented()) {
13685                    self.isAttached = !self.doDetach();
13686                }
13687                if (!self.isAttached) {
13688                    self.panel.trigger("media-detached", [self]);
13689                }
13690            }
13691            return !self.isAttached;
13692        },
13693        doDetach: function(){
13694            this.$el.detach();
13695            return true;
13696        },
13697        load: function(){
13698            var self = this, states = self.panel.cls.states;
13699            return $.Deferred(function(def){
13700                var e = self.panel.trigger("media-load", [self]);
13701                if (e.isDefaultPrevented()){
13702                    def.rejectWith("default prevented");
13703                    return;
13704                }
13705                if ( self.isLoaded ) {
13706                    def.resolve();
13707                } else if ( self.isError ) {
13708                    def.reject('previous error');
13709                } else {
13710                    self.isLoading = true;
13711                    self.$el.removeClass(states.allLoading).addClass(states.loading);
13712                    self.doLoad().then(def.resolve).fail(def.reject);
13713                }
13714            }).always(function(){
13715                self.isLoading = false;
13716                self.$el.removeClass(states.loading);
13717            }).then(function(){
13718                self.isLoaded = true;
13719                self.$el.addClass(states.loaded);
13720                self.panel.trigger("media-loaded", [self]);
13721            }).fail(function(){
13722                self.isError = true;
13723                self.$el.addClass(states.loaded);
13724                self.panel.trigger("media-error", [self]);
13725            }).promise();
13726        },
13727        doLoad: function(){
13728            return _fn.resolved;
13729        },
13730        unload: function(){
13731            var self = this;
13732            return $.Deferred(function(def){
13733                if (!self.isCreated || !self.isAttached){
13734                    def.rejectWith("not created or attached");
13735                    return;
13736                }
13737                var e = self.panel.trigger("media-unload", [self]);
13738                if (e.isDefaultPrevented()){
13739                    def.rejectWith("default prevented");
13740                    return;
13741                }
13742                self.doUnload().then(def.resolve).fail(def.reject);
13743            }).then(function(){
13744                self.panel.trigger("media-unloaded", [self]);
13745            }).promise();
13746        },
13747        doUnload: function(){
13748            return _fn.resolved;
13749        },
13750        resize: function(){}
13751    });
13752
13753    _.template.configure("core", {
13754        panel: {
13755            media: {
13756                width: null,
13757                height: null,
13758                minWidth: null,
13759                minHeight: null,
13760                maxWidth: null,
13761                maxHeight: null,
13762                attrs: {}
13763            }
13764        }
13765    },{
13766        panel: {
13767            media: {
13768                elem: "fg-media",
13769                type: "fg-media-unknown",
13770                content: "fg-media-content",
13771                caption: {
13772                    elem: "fg-media-caption",
13773                    title: "fg-media-caption-title",
13774                    description: "fg-media-caption-description",
13775                    exif: {
13776                        elem: "fg-media-caption-exif",
13777                        auto: "fg-media-caption-exif-auto",
13778                        full: "fg-media-caption-exif-full",
13779                        partial: "fg-media-caption-exif-partial",
13780                        minimal: "fg-media-caption-exif-minimal",
13781                        prop: "fg-media-caption-exif-prop",
13782                        icon: "fg-media-caption-exif-icon",
13783                        content: "fg-media-caption-exif-content",
13784                        label: "fg-media-caption-exif-label",
13785                        value: "fg-media-caption-exif-value",
13786                        tooltip: "fg-media-caption-exif-tooltip",
13787                        tooltipPointer: "fg-media-caption-exif-tooltip-pointer",
13788                        showTooltip: "fg-media-caption-exif-show-tooltip"
13789                    }
13790                },
13791                product: {
13792                    elem: "fg-media-product",
13793                    inner: "fg-media-product-inner",
13794                    header: "fg-media-product-header",
13795                    body: "fg-media-product-body",
13796                    footer: "fg-media-product-footer",
13797                    button: "fg-panel-button",
13798                    primary: "fg-panel-button-primary",
13799                    secondary: "fg-panel-button-secondary",
13800                    hidden: "fg-hidden",
13801                    disabled: "fg-disabled",
13802                    loading: "fg-loading"
13803                }
13804            }
13805        }
13806    }, {
13807        panel: {
13808            media: {
13809                product: {
13810                    title: "Product Information",
13811                    addToCart: "Add to Cart",
13812                    viewProduct: "View Product",
13813                    goToCheckout: "Go to Checkout",
13814                    success: "Successfully added to cart.",
13815                    error: "Something went wrong adding to cart."
13816                }
13817            }
13818        }
13819    });
13820
13821    _.Panel.media = new _.Factory();
13822
13823})(
13824    FooGallery.$,
13825    FooGallery,
13826    FooGallery.utils,
13827    FooGallery.utils.is,
13828    FooGallery.utils.fn,
13829    FooGallery.utils.obj,
13830    FooGallery.utils.str,
13831    FooGallery.utils.transition
13832);
13833
13834(function ($, _, _icons, _utils, _is, _fn, _obj, _str, _t) {
13835
13836    var canHover = !!window.matchMedia && window.matchMedia("(hover: hover)").matches;
13837
13838    _.Panel.Media.Caption = _utils.Class.extend({
13839        construct: function (panel, media) {
13840            var self = this;
13841            self.panel = panel;
13842            self.media = media;
13843            self.opt = panel.opt;
13844            self.cls = media.cls.caption;
13845            self.sel = media.sel.caption;
13846            self.$el = null;
13847            self.title = null;
13848            self.description = null;
13849            self.isCreated = false;
13850            self.isAttached = false;
13851            self.hasTitle = false;
13852            self.hasDescription = false;
13853            self.hasExif = false;
13854            self.init(media.item);
13855        },
13856        canLoad: function(){
13857            return this.hasTitle || this.hasDescription || this.hasExif;
13858        },
13859        init: function(item){
13860            if (!(item instanceof _.Item)) return;
13861            var self = this, title, desc, supplied = false;
13862            if (item.isCreated){
13863                var data = item.$anchor.data() || {};
13864                title = _is.string(data.lightboxTitle);
13865                desc = _is.string(data.lightboxDescription);
13866                if (title || desc){
13867                    supplied = true;
13868                    self.title = _.safeParse( title ? data.lightboxTitle : "" );
13869                    self.description = _.safeParse( desc ? data.lightboxDescription : "" );
13870                }
13871            } else {
13872                var attr = item.attr.anchor;
13873                title = _is.string(attr["data-lightbox-title"]);
13874                desc = _is.string(attr["data-lightbox-description"]);
13875                if (title || desc){
13876                    supplied = true;
13877                    self.title = _.safeParse( title ? attr["data-lightbox-title"] : "" );
13878                    self.description = _.safeParse( desc ? attr["data-lightbox-description"] : "" );
13879                }
13880            }
13881            if (!supplied){
13882                self.title = item.caption;
13883                self.description = item.description;
13884            }
13885            self.hasTitle = !_is.empty(self.title);
13886            self.hasDescription = !_is.empty(self.description);
13887            self.hasExif = item.hasExif && _utils.inArray(self.opt.exif, ["auto","full","partial","minimal"]) !== -1;
13888        },
13889        create: function(){
13890            if (!this.isCreated){
13891                var e = this.panel.trigger("caption-create", [this]);
13892                if (!e.isDefaultPrevented()){
13893                    this.isCreated = this.doCreate();
13894                    if (this.isCreated){
13895                        this.panel.trigger("caption-created", [this]);
13896                    }
13897                }
13898            }
13899            return this.isCreated;
13900        },
13901        doCreate: function(){
13902            var self = this;
13903            self.$el = $("<div/>").addClass(self.cls.elem);
13904            if (self.hasTitle){
13905                self.$el.append($("<div/>").addClass(self.cls.title).html(self.title));
13906            }
13907            if (self.hasDescription){
13908                self.$el.append($("<div/>").addClass(self.cls.description).html(self.description));
13909            }
13910            if (self.hasExif){
13911                var exif = self.media.item.exif, $exif = $("<div/>", {"class": self.cls.exif.elem}).addClass(self.cls.exif[self.opt.exif]);
13912                _.supportedExifProperties.forEach(function(prop){
13913                    if (!_is.empty(exif[prop])){
13914                        var icon = "exif-" + _str.kebab(prop), text = self.media.item.il8n.exif[prop], value = exif[prop];
13915                        var $exifProp = $("<div/>", {"class": self.cls.exif.prop}).append(
13916                            $("<div/>", {"class": self.cls.exif.icon}).append(_icons.get(icon, self.opt.icons)),
13917                            $("<div/>", {"class": self.cls.exif.content}).append(
13918                                $("<div/>", {"class": self.cls.exif.label}).text(text),
13919                                $("<div/>", {"class": self.cls.exif.value}).text(value)
13920                            ),
13921                            $("<span/>", {"class": self.cls.exif.tooltip}).text(text + ": " + value).append(
13922                                $("<span/>", {"class": self.cls.exif.tooltipPointer})
13923                            )
13924                        );
13925                        if (!canHover){
13926                            $exifProp.on("click", {self: self}, self.onExifClick);
13927                        }
13928                        $exif.append($exifProp);
13929                    }
13930                });
13931                self.$el.append($exif);
13932            }
13933            return true;
13934        },
13935        onExifClick: function(e){
13936            e.preventDefault();
13937            var self = e.data.self, $this = $(this),
13938                $tooltip = $this.find(self.sel.exif.tooltip),
13939                $current = $(self.sel.exif.showTooltip);
13940
13941            $(self.sel.exif.prop).removeClass(self.cls.exif.showTooltip)
13942                .find(self.sel.exif.tooltip).css("left", "")
13943                .find(self.sel.exif.tooltipPointer).css("left", "");
13944            if (!$current.is($this)){
13945                $tooltip.css("display", "inline-block");
13946
13947                var left = $tooltip.offset().left,
13948                    right = left + $tooltip.outerWidth(),
13949                    diff = Math.ceil(right - window.innerWidth);
13950
13951                if (diff > 0){
13952                    $tooltip.css("left", "calc(50% - " + diff + "px)")
13953                        .find(self.sel.exif.tooltipPointer).css("left", "calc(50% + " + diff + "px)");
13954                }
13955                if (left < 0){
13956                    left = Math.abs(left);
13957                    $tooltip.css("left", "calc(50% + " + left + "px)")
13958                        .find(self.sel.exif.tooltipPointer).css("left", "calc(50% - " + left + "px)");
13959                }
13960
13961                $tooltip.css("display", "");
13962                $this.addClass(self.cls.exif.showTooltip);
13963            }
13964        },
13965        destroy: function(){
13966            if (this.isCreated){
13967                var e = this.panel.trigger("caption-destroy", [this]);
13968                if (!e.isDefaultPrevented()){
13969                    this.isCreated = !this.doDestroy();
13970                    if (!this.isCreated){
13971                        this.panel.trigger("caption-destroyed", [this]);
13972                    }
13973                }
13974            }
13975            return !this.isCreated;
13976        },
13977        doDestroy: function(){
13978            this.$el.remove();
13979            return true;
13980        },
13981        appendTo: function( parent ){
13982            var self = this;
13983            if (!self.isCreated){
13984                self.create();
13985            }
13986            if (self.isCreated && !self.isAttached){
13987                var e = self.panel.trigger("caption-append", [self, parent]);
13988                if (!e.isDefaultPrevented()) {
13989                    self.isAttached = self.doAppendTo( parent );
13990                }
13991                if (self.isAttached) {
13992                    self.panel.trigger("caption-appended", [self, parent]);
13993                }
13994            }
13995            return self.isAttached;
13996        },
13997        doAppendTo: function( parent ){
13998            this.$el.appendTo( parent );
13999            return this.$el.parent().length > 0;
14000        },
14001        detach: function(){
14002            var self = this;
14003            if (self.isCreated && self.isAttached) {
14004                var e = self.panel.trigger("caption-detach", [self]);
14005                if (!e.isDefaultPrevented()) {
14006                    self.isAttached = !self.doDetach();
14007                }
14008                if (!self.isAttached) {
14009                    self.panel.trigger("caption-detached", [self]);
14010                }
14011            }
14012            return !self.isAttached;
14013        },
14014        doDetach: function(){
14015            this.$el.detach();
14016            return true;
14017        },
14018        load: function(){
14019            var self = this, states = self.panel.cls.states;
14020            return $.Deferred(function(def){
14021                var e = self.panel.trigger("caption-load", [self]);
14022                if (e.isDefaultPrevented()){
14023                    def.rejectWith("default prevented");
14024                    return;
14025                }
14026                self.$el.removeClass(states.allLoading).addClass(states.loading);
14027                self.doLoad().then(def.resolve).fail(def.reject);
14028            }).always(function(){
14029                self.$el.removeClass(states.loading);
14030            }).then(function(){
14031                self.$el.addClass(states.loaded);
14032                self.panel.trigger("caption-loaded", [self]);
14033            }).fail(function(){
14034                self.$el.addClass(states.loaded);
14035                self.panel.trigger("caption-error", [self]);
14036            }).promise();
14037        },
14038        doLoad: function(){
14039            return _fn.resolved;
14040        },
14041        unload: function(){
14042            var self = this;
14043            return $.Deferred(function(def){
14044                if (!self.isCreated || !self.isAttached){
14045                    def.rejectWith("not created or attached");
14046                    return;
14047                }
14048                var e = self.panel.trigger("caption-unload", [self]);
14049                if (e.isDefaultPrevented()){
14050                    def.rejectWith("default prevented");
14051                    return;
14052                }
14053                self.doUnload().then(def.resolve).fail(def.reject);
14054            }).then(function(){
14055                self.panel.trigger("caption-unloaded", [self]);
14056            }).promise();
14057        },
14058        doUnload: function(){
14059            return _fn.resolved;
14060        }
14061    });
14062
14063})(
14064    FooGallery.$,
14065    FooGallery,
14066    FooGallery.icons,
14067    FooGallery.utils,
14068    FooGallery.utils.is,
14069    FooGallery.utils.fn,
14070    FooGallery.utils.obj,
14071    FooGallery.utils.str,
14072    FooGallery.utils.transition
14073);
14074
14075(function ($, _, _utils, _is, _fn, _obj, _t) {
14076
14077    _.Panel.Media.Product = _utils.Class.extend({
14078        construct: function (panel, media) {
14079            var self = this;
14080            self.panel = panel;
14081            self.media = media;
14082            self.opt = panel.opt;
14083            self.cls = media.cls.product;
14084            self.sel = media.sel.product;
14085            self.il8n = media.il8n.product;
14086            self.$el = null;
14087            self.$inner = null;
14088            self.$header = null;
14089            self.$body = null;
14090            self.$footer = null;
14091            self.isCreated = false;
14092            self.isAttached = false;
14093            self.__loaded = null;
14094            self.__requestId = null;
14095        },
14096        hasWooCommerce: function(){
14097            return !!window.woocommerce_params;
14098        },
14099        canLoad: function(){
14100            return !_is.empty(this.media.item.productId) && ((this.panel.opt.admin && !this.hasWooCommerce()) || this.hasWooCommerce());
14101        },
14102        create: function(){
14103            var self = this;
14104            if (!self.isCreated){
14105                var e = self.panel.trigger("product-create", [self]);
14106                if (!e.isDefaultPrevented()){
14107                    self.isCreated = self.doCreate();
14108                    if (self.isCreated){
14109                        self.panel.trigger("product-created", [self]);
14110                    }
14111                }
14112            }
14113            return self.isCreated;
14114        },
14115        doCreate: function(){
14116            var self = this;
14117            self.$el = $("<div/>").addClass(self.cls.elem).append(
14118                $("<div/>").addClass(self.panel.cls.loader)
14119            );
14120            self.$inner = $("<div/>").addClass(self.cls.inner).appendTo(self.$el);
14121            self.$header = $("<div/>").addClass(self.cls.header).html(self.il8n.title).appendTo(self.$inner);
14122            self.$body = $("<div/>").addClass(self.cls.body).appendTo(self.$inner);
14123            self.$addToCart = $("<button/>").addClass(self.cls.button).addClass(self.cls.primary).html(self.il8n.addToCart).on("click", {self: self}, self.onAddToCartClick);
14124            self.$viewProduct = $("<a/>").addClass(self.cls.button).addClass(self.cls.secondary).html(self.il8n.viewProduct);
14125            self.$goToCheckout = $("<a/>").addClass(self.cls.button).addClass(self.cls.secondary).html(self.il8n.goToCheckout);
14126            self.$footer = $("<div/>").addClass(self.cls.footer).append(self.$addToCart).append(self.$viewProduct).append(self.$goToCheckout).appendTo(self.$inner);
14127            return true;
14128        },
14129        destroy: function(){
14130            var self = this;
14131            if (self.isCreated){
14132                var e = self.panel.trigger("product-destroy", [self]);
14133                if (!e.isDefaultPrevented()){
14134                    self.isCreated = !self.doDestroy();
14135                    if (!self.isCreated){
14136                        self.panel.trigger("product-destroyed", [self]);
14137                    }
14138                }
14139            }
14140            return !self.isCreated;
14141        },
14142        doDestroy: function(){
14143            this.$el.remove();
14144            return true;
14145        },
14146        appendTo: function( parent ){
14147            var self = this;
14148            if (!self.isCreated){
14149                self.create();
14150            }
14151            if (self.isCreated && !self.isAttached){
14152                var e = self.panel.trigger("product-append", [self, parent]);
14153                if (!e.isDefaultPrevented()) {
14154                    self.isAttached = self.doAppendTo( parent );
14155                }
14156                if (self.isAttached) {
14157                    self.panel.trigger("product-appended", [self, parent]);
14158                }
14159            }
14160            return self.isAttached;
14161        },
14162        doAppendTo: function( parent ){
14163            this.$el.appendTo( parent );
14164            return this.$el.parent().length > 0;
14165        },
14166        detach: function(){
14167            var self = this;
14168            if (self.isCreated && self.isAttached) {
14169                var e = self.panel.trigger("product-detach", [self]);
14170                if (!e.isDefaultPrevented()) {
14171                    self.isAttached = !self.doDetach();
14172                }
14173                if (!self.isAttached) {
14174                    self.panel.trigger("product-detached", [self]);
14175                }
14176            }
14177            return !self.isAttached;
14178        },
14179        doDetach: function(){
14180            this.$el.detach();
14181            return true;
14182        },
14183        load: function(){
14184            var self = this, states = self.panel.cls.states;
14185            return $.Deferred(function(def){
14186                var e = self.panel.trigger("product-load", [self]);
14187                if (e.isDefaultPrevented()){
14188                    def.rejectWith("default prevented");
14189                    return;
14190                }
14191                self.$el.removeClass(states.allLoading).addClass(states.loading);
14192                self.doLoad().then(def.resolve).fail(def.reject);
14193            }).always(function(){
14194                self.$el.removeClass(states.loading);
14195            }).then(function(){
14196                self.$el.addClass(states.loaded);
14197                self.panel.trigger("product-loaded", [self]);
14198            }).fail(function(){
14199                self.$el.addClass(states.loaded);
14200                self.panel.trigger("product-error", [self]);
14201            }).promise();
14202        },
14203        doLoad: function(){
14204            var self = this;
14205            if (self.__loaded !== null) return self.__loaded;
14206            return self.__loaded = $.ajax({
14207                type: "POST",
14208                url: self.panel.opt.cartAjax,
14209                data: {
14210                    action: "foogallery_product_variations",
14211                    nonce: self.panel.opt.cartNonce,
14212                    nonce_time: self.panel.opt.cartTimeout,
14213                    product_id: self.media.item.productId,
14214                    gallery_id: self.panel.tmpl.id,
14215                    attachment_id: self.media.item.id
14216                }
14217            }).then(function(response){
14218                if (response.error){
14219                    console.log("Error fetching product information from server.", response.error);
14220                    self.$footer.addClass(self.cls.hidden);
14221                }
14222                if (self.panel.opt.admin){
14223                    self.$addToCart.toggleClass(self.cls.disabled, true);
14224                } else {
14225                    self.$addToCart.toggleClass(self.cls.hidden, !self.hasWooCommerce() || !response.purchasable);
14226                }
14227                if (_is.string(response.product_url)){
14228                    if (self.panel.opt.admin){
14229                        self.$viewProduct.toggleClass(self.cls.disabled, true);
14230                    } else {
14231                        self.$viewProduct.prop("href", response.product_url);
14232                    }
14233                } else {
14234                    self.$viewProduct.toggleClass(self.cls.hidden, true);
14235                }
14236                if (_is.string(response.checkout_url)){
14237                    if (self.panel.opt.admin){
14238                        self.$goToCheckout.toggleClass(self.cls.disabled, true);
14239                    } else {
14240                        self.$goToCheckout.prop("href", response.checkout_url);
14241                    }
14242                } else {
14243                    self.$goToCheckout.toggleClass(self.cls.hidden, true);
14244                }
14245                self.$body.html(response.body).find("tr").on("click", {self: self}, self.onRowClick);
14246                if (_is.string(response.title)){
14247                    self.$header.html(response.title);
14248                } else {
14249                    self.$header.html(self.il8n.title);
14250                }
14251            }).promise();
14252        },
14253        unload: function(){
14254            var self = this;
14255            return $.Deferred(function(def){
14256                if (!self.isCreated || !self.isAttached){
14257                    def.rejectWith("not created or attached");
14258                    return;
14259                }
14260                var e = self.panel.trigger("product-unload", [self]);
14261                if (e.isDefaultPrevented()){
14262                    def.rejectWith("default prevented");
14263                    return;
14264                }
14265                self.doUnload().then(def.resolve).fail(def.reject);
14266            }).then(function(){
14267                self.panel.trigger("product-unloaded", [self]);
14268            }).promise();
14269        },
14270        doUnload: function(){
14271
14272            return _fn.resolved;
14273        },
14274        onAddToCartClick: function(e){
14275            e.preventDefault();
14276            var $this = $(this),
14277                self = e.data.self,
14278                variation_id = self.$body.find(":radio:checked").val(),
14279                product_id = variation_id || self.media.item.productId;
14280
14281            self.$addToCart.addClass(self.cls.disabled).addClass(self.cls.loading);
14282            self.media.item.addToCart($this, product_id, 1, false).then(function(response){
14283                let $msg;
14284                if (!response || response.error){
14285                    $msg = $("<p>" + self.il8n.error + "</p>");
14286                } else {
14287                    $msg = $("<p>" + self.il8n.success + "</p>");
14288                }
14289                self.$footer.append($msg);
14290                setTimeout( () => {
14291                    $msg.remove();
14292                }, 3000 );
14293            }).always(function(){
14294                self.$addToCart.removeClass(self.cls.disabled).removeClass(self.cls.loading);
14295            });
14296        },
14297        onRowClick: function(e){
14298            if (!$(e.target).is(":radio")){
14299                e.preventDefault();
14300                e.stopPropagation();
14301                $(this).find(":radio").prop("checked", true);
14302            }
14303        }
14304    });
14305
14306})(
14307    FooGallery.$,
14308    FooGallery,
14309    FooGallery.utils,
14310    FooGallery.utils.is,
14311    FooGallery.utils.fn,
14312    FooGallery.utils.obj,
14313    FooGallery.utils.transition
14314);
14315
14316(function($, _, _utils, _is, _obj){
14317
14318    _.Panel.Image = _.Panel.Media.extend({
14319        construct: function(panel, item){
14320            this._super(panel, item);
14321            _obj.extend(this.opt, panel.opt.image);
14322            _obj.extend(this.cls, panel.cls.image);
14323            _obj.extend(this.sel, panel.sel.image);
14324            this.allFullClasses = [this.cls.fullWidth, this.cls.fullHeight].join(" ");
14325        },
14326        doCreateContent: function(){
14327            return $('<img/>').attr(this.opt.attrs);
14328        },
14329        resize: function(){
14330            var self = this;
14331            if (self.isCreated && self.panel.opt.fitMedia){
14332                var img = self.$content.get(0);
14333                if (img.naturalWidth && img.naturalHeight){
14334                    var landscape = img.naturalWidth >= img.naturalHeight,
14335                        fullWidth = landscape,
14336                        targetWidth = self.$el.innerWidth(),
14337                        targetHeight = self.$el.innerHeight(),
14338                        ratio;
14339
14340                    if (landscape){
14341                        ratio = targetWidth / img.naturalWidth;
14342                        if (img.naturalHeight * ratio < targetHeight){
14343                            fullWidth = false;
14344                        }
14345                    } else {
14346                        ratio = targetHeight / img.naturalHeight;
14347                        if (img.naturalWidth * ratio < targetWidth){
14348                            fullWidth = true;
14349                        }
14350                    }
14351                    _utils.requestFrame(function(){
14352                        self.$content.removeClass(self.allFullClasses).addClass(fullWidth ? self.cls.fullWidth : self.cls.fullHeight);
14353                    });
14354                }
14355            }
14356        },
14357        doLoad: function(){
14358            var self = this;
14359            return $.Deferred(function(def){
14360                var img = self.$content.get(0);
14361                img.alt = self.item.alt;
14362                img.onload = function () {
14363                    img.onload = img.onerror = null;
14364                    def.resolve(self);
14365                };
14366                img.onerror = function () {
14367                    img.onload = img.onerror = null;
14368                    def.rejectWith("error loading image");
14369                };
14370                const { opt: { cors } } = self.panel.tmpl;
14371                if ( _is.string( cors ) ) {
14372                    if ( img.crossOrigin === null && _.isCrossOrigin(self.item.href) ) {
14373                        img.crossOrigin = cors;
14374                    }
14375                }
14376                // set everything in motion by setting the src
14377                img.src = self.item.href;
14378                if (img.complete){
14379                    img.onload();
14380                }
14381            }).then(function(){
14382                self.resize();
14383            }).promise();
14384        }
14385    });
14386
14387    _.Panel.media.register("image", _.Panel.Image);
14388
14389    _.template.configure("core", {
14390        panel: {
14391            image: {
14392                attrs: {
14393                    draggable: false
14394                }
14395            }
14396        }
14397    },{
14398        panel: {
14399            image: {
14400                type: "fg-media-image",
14401                fullWidth: "fg-media-full-width",
14402                fullHeight: "fg-media-full-height"
14403            }
14404        }
14405    });
14406
14407})(
14408    FooGallery.$,
14409    FooGallery,
14410    FooGallery.utils,
14411    FooGallery.utils.is,
14412    FooGallery.utils.obj
14413);
14414(function($, _, _utils, _obj){
14415
14416    _.Panel.Iframe = _.Panel.Media.extend({
14417        construct: function(panel, item){
14418            this._super(panel, item);
14419            _obj.extend(this.opt, panel.opt.iframe);
14420            _obj.extend(this.cls, panel.cls.iframe);
14421            _obj.extend(this.sel, panel.sel.iframe);
14422        },
14423        doCreateContent: function(){
14424            return $('<iframe/>').attr(this.opt.attrs);
14425        },
14426        doLoad: function(){
14427            var self = this;
14428            return $.Deferred(function(def){
14429                self.$content.off("load error").on({
14430                    'load': function(){
14431                        self.$content.off("load error");
14432                        def.resolve(self);
14433                    },
14434                    'error': function(){
14435                        self.$content.off("load error");
14436                        def.reject(self);
14437                    }
14438                });
14439                self.$content.attr("src", self.item.href);
14440            }).promise();
14441        }
14442    });
14443
14444    _.Panel.media.register("iframe", _.Panel.Iframe);
14445
14446    _.template.configure("core", {
14447        panel: {
14448            iframe: {
14449                attrs: {
14450                    src: '',
14451                    frameborder: 'no',
14452                    allow: "autoplay; fullscreen",
14453                    allowfullscreen: true
14454                }
14455            }
14456        }
14457    },{
14458        panel: {
14459            iframe: {
14460                type: "fg-media-iframe"
14461            }
14462        }
14463    });
14464
14465})(
14466    FooGallery.$,
14467    FooGallery,
14468    FooGallery.utils,
14469    FooGallery.utils.obj
14470);
14471(function($, _, _utils, _obj, _str){
14472
14473    _.Panel.Html = _.Panel.Media.extend({
14474        construct: function(panel, item){
14475            this._super(panel, item);
14476            _obj.extend(this.opt, panel.opt.html);
14477            _obj.extend(this.cls, panel.cls.html);
14478            _obj.extend(this.sel, panel.sel.html);
14479            this.$target = null;
14480        },
14481        doCreate: function(){
14482            if (this._super()){
14483                if (!_str.startsWith(this.item.href, '#') || (this.$target = $(this.item.href)).length === 0){
14484                    this.$target = null;
14485                    return false;
14486                }
14487                return true;
14488            }
14489            return false;
14490        },
14491        doCreateContent: function(){
14492            return $('<div/>').attr(this.opt.attrs);
14493        },
14494        doAppendTo: function( parent ){
14495            if (this._super( parent )){
14496                this.$content.append(this.$target.contents());
14497                return true;
14498            }
14499            return false;
14500        },
14501        doDetach: function(){
14502            this.$target.append(this.$content.contents());
14503            return this._super();
14504        }
14505    });
14506
14507    _.Panel.media.register("html", _.Panel.Html);
14508
14509    _.template.configure("core", {
14510        panel: {
14511            html: {}
14512        }
14513    },{
14514        panel: {
14515            html: {
14516                type: "fg-media-html"
14517            }
14518        }
14519    });
14520
14521})(
14522    FooGallery.$,
14523    FooGallery,
14524    FooGallery.utils,
14525    FooGallery.utils.obj,
14526    FooGallery.utils.str
14527);
14528(function($, _, _utils, _obj, _str){
14529
14530    _.Panel.Embed = _.Panel.Html.extend({
14531        construct: function(panel, item){
14532            this._super(panel, item);
14533            _obj.extend(this.opt, panel.opt.embed);
14534            _obj.extend(this.cls, panel.cls.embed);
14535            _obj.extend(this.sel, panel.sel.embed);
14536        }
14537    });
14538
14539    _.Panel.media.register("embed", _.Panel.Embed);
14540
14541    _.template.configure("core", {
14542        panel: {
14543            embed: {}
14544        }
14545    },{
14546        panel: {
14547            embed: {
14548                type: "fg-media-embed"
14549            }
14550        }
14551    });
14552
14553})(
14554    FooGallery.$,
14555    FooGallery,
14556    FooGallery.utils,
14557    FooGallery.utils.obj,
14558    FooGallery.utils.str
14559);
14560(function($, _, _utils, _is, _obj, _url){
14561
14562    _.Panel.Video = _.Panel.Media.extend({
14563        construct: function(panel, item){
14564            this._super(panel, item);
14565            _obj.extend(this.opt, panel.opt.video);
14566            _obj.extend(this.cls, panel.cls.video);
14567            _obj.extend(this.sel, panel.sel.video);
14568            this.urls = [];
14569            this.isSelfHosted = false;
14570        },
14571        parseHref: function(){
14572            var self = this, urls = self.item.href.split(','), result = [];
14573            for (var i = 0, il = urls.length, url, source; i < il; i++){
14574                if (_is.empty(urls[i])) continue;
14575                url = _url.parts(urls[i]);
14576                source = null;
14577                for (var j = 0, jl = self.panel.videoSources.length; j < jl; j++){
14578                    if (self.panel.videoSources[j].canPlay(url)){
14579                        source = self.panel.videoSources[j];
14580                        result.push({
14581                            parts: url,
14582                            source: source,
14583                            embed: source.getEmbedUrl(url, self.opt.autoPlay)
14584                        });
14585                        break;
14586                    }
14587                }
14588            }
14589            return result;
14590        },
14591        doCreateContent: function(){
14592            this.urls = this.parseHref();
14593            this.isSelfHosted = $.map(this.urls, function(url){ return url.source.selfHosted ? true : null; }).length > 0;
14594
14595            const { opt: { cors } } = this.panel.tmpl;
14596            const attr = { ...this.opt.attrs.video };
14597            if ( _is.string( cors ) ) {
14598                const first = this.urls.at( 0 );
14599                if ( _is.string( first ) && _is.string( attr?.crossOrigin ) && _.isCrossOrigin( first ) ) {
14600                    attr.crossOrigin = cors;
14601                }
14602            }
14603            return this.isSelfHosted
14604                ? $('<video/>', attr)
14605                : $('<iframe/>', this.opt.attrs.iframe).addClass("fitvidsignore");
14606        },
14607        doLoad: function(){
14608            var self = this;
14609            return $.Deferred(function(def){
14610                if (self.urls.length === 0){
14611                    def.rejectWith("no urls available");
14612                    return;
14613                }
14614                var promise = self.isSelfHosted ? self.loadSelfHosted() : self.loadIframe();
14615                promise.then(def.resolve).fail(def.reject);
14616            }).promise();
14617        },
14618        loadSelfHosted: function(){
14619            var self = this;
14620            return $.Deferred(function(def){
14621                self.$content.off("loadeddata error");
14622                self.$content.find("source").remove();
14623                if (!_is.empty(self.item.cover)){
14624                    self.$content.attr("poster", self.item.cover);
14625                }
14626                self.$content.on({
14627                    'loadeddata': function(){
14628                        self.$content.off("loadeddata error");
14629                        this.volume = self.opt.volume;
14630                        if (self.opt.autoPlay){
14631                            var p = this.play();
14632                            if (typeof p !== 'undefined'){
14633                                p.catch(function(){
14634                                    console.log("Unable to autoplay video due to policy changes: https://developers.google.com/web/updates/2017/09/autoplay-policy-changes");
14635                                });
14636                            }
14637                        }
14638                        def.resolve(self);
14639                    },
14640                    'error': function(){
14641                        self.$content.off("loadeddata error");
14642                        def.reject(self);
14643                    }
14644                });
14645                var sources = $.map(self.urls, function(url){
14646                    return $("<source/>", {src: url.embed, mimeType: url.source.mimeType});
14647                });
14648                self.$content.append(sources);
14649                self.$content.get(0).load();
14650            }).promise();
14651        },
14652        loadIframe: function(){
14653            var self = this;
14654            return $.Deferred(function(def){
14655                if (!_is.empty(self.item.cover)){
14656                    self.$content.css("background-image", "url('" + self.item.cover + "')");
14657                }
14658                self.$content.off("load error").on({
14659                    'load': function(){
14660                        self.$content.off("load error");
14661                        def.resolve(self);
14662                    },
14663                    'error': function(){
14664                        self.$content.off("load error");
14665                        def.reject(self);
14666                    }
14667                });
14668                self.$content.attr("src", self.urls[0].embed);
14669            }).promise();
14670        }
14671    });
14672
14673    _.Panel.media.register("video", _.Panel.Video);
14674
14675    _.template.configure("core", {
14676        panel: {
14677            video: {
14678                autoPlay: false,
14679                volume: 0.2,
14680                privacyEnhanced: true, // specifically for YouTube to switch between the nocookie and standard url
14681                attrs: {
14682                    iframe: {
14683                        src: '',
14684                        frameborder: 'no',
14685                        allow: "autoplay; fullscreen",
14686                        allowfullscreen: true
14687                    },
14688                    video: {
14689                        controls: true,
14690                        preload: false,
14691                        controlsList: "nodownload"
14692                    }
14693                }
14694            }
14695        }
14696    },{
14697        panel: {
14698            video: {
14699                type: "fg-media-video"
14700            }
14701        }
14702    });
14703
14704})(
14705    FooGallery.$,
14706    FooGallery,
14707    FooGallery.utils,
14708    FooGallery.utils.is,
14709    FooGallery.utils.obj,
14710    FooGallery.utils.url
14711);
14712
14713(function($, _, _utils, _is, _url, _str){
14714
14715    var videoEl = document.createElement("video");
14716
14717    _.Panel.Video.Source = _utils.Class.extend({
14718        construct: function(panel, mimeType, regex, selfHosted, embedParams, autoPlayParam){
14719            this.panel = panel;
14720            this.mimeType = mimeType;
14721            this.regex = regex;
14722            this.selfHosted = _is.boolean(selfHosted) ? selfHosted : false;
14723            this.embedParams = _is.array(embedParams) ? embedParams : [];
14724            this.autoPlayParam = _is.hash(autoPlayParam) ? autoPlayParam : {};
14725            this.canPlayType = this.selfHosted && _is.fn(videoEl.canPlayType) ? _utils.inArray(videoEl.canPlayType(this.mimeType), ['probably','maybe']) !== -1 : true;
14726        },
14727        canPlay: function(urlParts){
14728            return this.canPlayType && this.regex.test(urlParts.href);
14729        },
14730        mergeParams: function(urlParts, autoPlay){
14731            var self = this;
14732            for (var i = 0, il = self.embedParams.length, ip; i < il; i++){
14733                ip = self.embedParams[i];
14734                urlParts.search = _url.param(urlParts.search, ip.key, ip.value);
14735            }
14736            if (!_is.empty(self.autoPlayParam)){
14737                urlParts.search = _url.param(urlParts.search, self.autoPlayParam.key, autoPlay ? self.autoPlayParam.value : '');
14738            }
14739            return urlParts.search;
14740        },
14741        getId: function(urlParts){
14742            var match = urlParts.href.match(/.*\/(.*?)($|\?|#)/);
14743            return match && match.length >= 2 ? match[1] : null;
14744        },
14745        getEmbedUrl: function(urlParts, autoPlay){
14746            urlParts.search = this.mergeParams(urlParts, autoPlay);
14747            return _str.join('/', location.protocol, '//', urlParts.host, urlParts.pathname) + urlParts.search + urlParts.hash;
14748        }
14749    });
14750
14751    _.Panel.Video.sources = new _.Factory();
14752
14753})(
14754    FooGallery.$,
14755    FooGallery,
14756    FooGallery.utils,
14757    FooGallery.utils.is,
14758    FooGallery.utils.url,
14759    FooGallery.utils.str
14760);
14761(function(_){
14762
14763    _.Panel.Video.Dailymotion = _.Panel.Video.Source.extend({
14764        construct: function(panel){
14765            this._super(
14766                panel,
14767                'video/daily',
14768                /(www.)?dailymotion\.com|dai\.ly/i,
14769                false,
14770                [
14771                    {key: 'wmode', value: 'opaque'},
14772                    {key: 'info', value: '0'},
14773                    {key: 'logo', value: '0'},
14774                    {key: 'related', value: '0'}
14775                ],
14776                {key: 'autoplay', value: '1'}
14777            );
14778        },
14779        getId: function(urlParts){
14780            return /\/video\//i.test(urlParts.href)
14781                ? urlParts.href.split(/\/video\//i)[1].split(/[?&]/)[0].split(/[_]/)[0]
14782                : urlParts.href.split(/dai\.ly/i)[1].split(/[?&]/)[0];
14783        },
14784        getEmbedUrl: function(urlParts, autoPlay){
14785            var id = this.getId(urlParts);
14786            urlParts.search = this.mergeParams(urlParts, autoPlay);
14787            return 'https://www.dailymotion.com/embed/video/' + id + urlParts.search + urlParts.hash;
14788        }
14789    });
14790
14791    _.Panel.Video.sources.register('video/daily', _.Panel.Video.Dailymotion);
14792
14793})(
14794    FooGallery
14795);
14796(function(_){
14797
14798    _.Panel.Video.Mp4 = _.Panel.Video.Source.extend({
14799        construct: function(panel){
14800            this._super(panel, 'video/mp4', /\.mp4/i, true);
14801        }
14802    });
14803    _.Panel.Video.sources.register('video/mp4', _.Panel.Video.Mp4);
14804
14805    _.Panel.Video.Webm = _.Panel.Video.Source.extend({
14806        construct: function(panel){
14807            this._super(panel, 'video/webm', /\.webm/i, true);
14808        }
14809    });
14810    _.Panel.Video.sources.register('video/webm', _.Panel.Video.Webm);
14811
14812    _.Panel.Video.Wmv = _.Panel.Video.Source.extend({
14813        construct: function(panel){
14814            this._super(panel, 'video/wmv', /\.wmv/i, true);
14815        }
14816    });
14817    _.Panel.Video.sources.register('video/wmv', _.Panel.Video.Wmv);
14818
14819    _.Panel.Video.Ogv = _.Panel.Video.Source.extend({
14820        construct: function(panel){
14821            this._super(panel, 'video/ogg', /\.ogv|\.ogg/i, true);
14822        }
14823    });
14824    _.Panel.Video.sources.register('video/ogg', _.Panel.Video.Ogv);
14825
14826})(
14827    FooGallery
14828);
14829(function(_){
14830
14831    _.Panel.Video.Vimeo = _.Panel.Video.Source.extend({
14832        construct: function(panel){
14833            this._super(
14834                panel,
14835                'video/vimeo',
14836                /(player.)?vimeo\.com/i,
14837                false,
14838                [
14839                    {key: 'badge', value: '0'},
14840                    {key: 'portrait', value: '0'}
14841                ],
14842                {key: 'autoplay', value: '1'}
14843            );
14844        },
14845        getEmbedUrl: function(urlParts, autoPlay){
14846            var id = this.getId(urlParts);
14847            urlParts.search = this.mergeParams(urlParts, autoPlay);
14848            return 'https://player.vimeo.com/video/' + id + urlParts.search + urlParts.hash;
14849        }
14850    });
14851
14852    _.Panel.Video.sources.register('video/vimeo', _.Panel.Video.Vimeo);
14853
14854})(
14855    FooGallery
14856);
14857(function(_, _is, _url){
14858
14859    _.Panel.Video.Wistia = _.Panel.Video.Source.extend({
14860        construct: function(panel){
14861            this._super(
14862                panel,
14863                'video/wistia',
14864                /(.+)?(wistia\.(com|net)|wi\.st)\/.*/i,
14865                false,
14866                [],
14867                {
14868                    iframe: {key: 'autoPlay', value: '1'},
14869                    playlists: {key: 'media_0_0[autoPlay]', value: '1'}
14870                }
14871            );
14872        },
14873        getType: function(href){
14874            return /playlists\//i.test(href) ? 'playlists' : 'iframe';
14875        },
14876        mergeParams: function(urlParts, autoPlay){
14877            var self = this;
14878            for (var i = 0, il = self.embedParams.length, ip; i < il; i++){
14879                ip = self.embedParams[i];
14880                urlParts.search = _url.param(urlParts.search, ip.key, ip.value);
14881            }
14882            if (!_is.empty(self.autoPlayParam)){
14883                var param = self.autoPlayParam[self.getType(urlParts.href)];
14884                urlParts.search = _url.param(urlParts.search, param.key, autoPlay ? param.value : '');
14885            }
14886            return urlParts.search;
14887        },
14888        getId: function(urlParts){
14889            return /embed\//i.test(urlParts.href)
14890                ? urlParts.href.split(/embed\/.*?\//i)[1].split(/[?&]/)[0]
14891                : urlParts.href.split(/medias\//)[1].split(/[?&]/)[0];
14892        },
14893        getEmbedUrl: function(urlParts, autoPlay){
14894            var id = this.getId(urlParts);
14895            urlParts.search = this.mergeParams(urlParts, autoPlay);
14896            return 'https://fast.wistia.net/embed/'+this.getType(urlParts.href)+'/' + id + urlParts.search + urlParts.hash;
14897        }
14898    });
14899
14900    _.Panel.Video.sources.register('video/wistia', _.Panel.Video.Wistia);
14901
14902})(
14903    FooGallery,
14904    FooGallery.utils.is,
14905    FooGallery.utils.url
14906);
14907(function(_){
14908
14909    _.Panel.Video.YouTube = _.Panel.Video.Source.extend({
14910        construct: function(panel){
14911            this._super(
14912                panel,
14913                'video/youtube',
14914                /(www.)?youtube|youtu\.be/i,
14915                false,
14916                [
14917                    {key: 'modestbranding', value: '1'},
14918                    {key: 'rel', value: '0'},
14919                    {key: 'wmode', value: 'transparent'},
14920                    {key: 'showinfo', value: '0'}
14921                ],
14922                {key: 'autoplay', value: '1'}
14923            );
14924        },
14925        getId: function(urlParts){
14926            if ( /shorts\//i.test(urlParts.href) ){
14927                return urlParts.href.split(/shorts\//i)[1].split(/[?&]/)[0];
14928            }
14929            return /embed\//i.test(urlParts.href)
14930                ? urlParts.href.split(/embed\//i)[1].split(/[?&]/)[0]
14931                : urlParts.href.split(/v\/|v=|youtu\.be\//i)[1].split(/[?&]/)[0];
14932        },
14933        getEmbedUrl: function(urlParts, autoPlay){
14934            var id = this.getId(urlParts);
14935            urlParts.search = this.mergeParams(urlParts, autoPlay);
14936            return 'https://www.youtube' + ( this.panel.opt.video.privacyEnhanced ? '-nocookie' : '' ) + '.com/embed/' + id + urlParts.search + urlParts.hash;
14937        }
14938    });
14939
14940    _.Panel.Video.sources.register('video/youtube', _.Panel.Video.YouTube);
14941
14942})(
14943    FooGallery
14944);
14945(function(_){
14946
14947    _.Panel.Video.TED = _.Panel.Video.Source.extend({
14948        construct: function(panel){
14949            this._super(
14950                panel,
14951                'video/ted',
14952                /(www.)?ted\.com/i,
14953                false,
14954                [],
14955                {key: 'autoplay', value: '1'}
14956            );
14957        },
14958        getEmbedUrl: function(urlParts, autoPlay){
14959            var id = this.getId(urlParts);
14960            urlParts.search = this.mergeParams(urlParts, autoPlay);
14961            return 'https://embed.ted.com/talks/' + id + urlParts.search + urlParts.hash;
14962        }
14963    });
14964
14965    _.Panel.Video.sources.register('video/ted', _.Panel.Video.TED);
14966
14967})(
14968    FooGallery
14969);
14970(function(_, _url){
14971
14972    _.Panel.Video.Facebook = _.Panel.Video.Source.extend({
14973        construct: function(panel){
14974            this._super(
14975                panel,
14976                'video/facebook',
14977                /(www.)?facebook\.com\/.*?\/videos\//i,
14978                false,
14979                [
14980                    {key: 'show_text', value: '0'},
14981                    {key: 'show_caption', value: '0'}
14982                ],
14983                {key: 'autoplay', value: '1'}
14984            );
14985        },
14986        getEmbedUrl: function(urlParts, autoPlay){
14987            var search = _url.param(this.mergeParams(urlParts, autoPlay), "href", encodeURI(urlParts.origin + urlParts.pathname));
14988            return 'https://www.facebook.com/plugins/video.php' + search + urlParts.hash;
14989        }
14990    });
14991
14992    _.Panel.Video.sources.register('video/facebook', _.Panel.Video.Facebook);
14993
14994})(
14995    FooGallery,
14996    FooGallery.utils.url
14997);
14998(function(_){
14999
15000    _.Panel.Video.Generic = _.Panel.Video.Source.extend({
15001        construct: function(panel){
15002            this._super(
15003                panel,
15004                'video/generic',
15005                /[&?]fg_video=(1|true)(&|$)/i
15006            );
15007        },
15008        getEmbedUrl: function(urlParts, autoPlay){
15009            return urlParts.href;
15010        }
15011    });
15012
15013    _.Panel.Video.sources.register('video/generic', _.Panel.Video.Generic);
15014
15015})(
15016    FooGallery
15017);
15018(function($, _, _is, _obj){
15019
15020    _.Lightbox = _.Panel.extend({
15021        construct: function (template, options) {
15022            var self = this;
15023            self._super(template, options);
15024            if (self.opt.enabled && (self.tmpl instanceof _.Template) && !(self.tmpl.destroying || self.tmpl.destroyed)) {
15025                self.tmpl.on({
15026                    "after-state": self.onAfterState,
15027                    "anchor-click-item": self.onAnchorClickItem,
15028                    "destroyed": self.onDestroyedTemplate
15029                }, self);
15030            }
15031        },
15032        onAnchorClickItem: function(e, item){
15033            if (!item.noLightbox){
15034                e.preventDefault();
15035                try {
15036                    this.open(item);
15037                } catch( err ) {
15038                    console.error( err );
15039                }
15040            }
15041        },
15042        onDestroyedTemplate: function(){
15043            this.destroy();
15044        },
15045        onAfterState: function(e, state){
15046            if (state.item instanceof _.Item && !state.item.noLightbox){
15047                try {
15048                    this.open(state.item);
15049                } catch( err ) {
15050                    console.error( err );
15051                }
15052            }
15053        }
15054    });
15055
15056    _.template.configure("core", {
15057        lightbox: {
15058            enabled: false
15059        }
15060    }, {});
15061
15062    _.Template.override("construct", function(options, element){
15063        this._super(options, element);
15064        var data = this.$el.data("foogalleryLightbox"),
15065            enabled = this.opt.lightbox.enabled || _is.hash(data) || (this.$el.length > 0 && this.el.hasAttribute("data-foogallery-lightbox"));
15066
15067        this.opt.lightbox = _obj.extend({}, this.opt.panel, this.opt.lightbox, { enabled: enabled }, data, this.opt.mobileLightbox);
15068        this.lightbox = enabled ? new _.Lightbox(this, this.opt.lightbox) : null;
15069    });
15070
15071})(
15072    FooGallery.$,
15073    FooGallery,
15074    FooGallery.utils.is,
15075    FooGallery.utils.obj
15076);
15077
15078(function($, _, _is){
15079
15080    _.template.configure("core", {}, {
15081        woo: {
15082            button: "fg-woo-add-to-cart-ajax",
15083            disabled: "fg-disabled",
15084            added: "fg-woo-added",
15085            adding: "fg-woo-adding"
15086        }
15087    });
15088
15089    _.Item.prototype.hasWooCommerce = function(){
15090        return !!window.woocommerce_params;
15091    };
15092
15093    _.Item.prototype.getWooCommerceEndPoint = function(){
15094        return this.hasWooCommerce() ? window.woocommerce_params.wc_ajax_url.toString().replace('%%endpoint%%', 'add_to_cart') : '';
15095    };
15096
15097    _.Item.prototype.onAddToCart = function(e){
15098        var self = e.data.self, $this = $(this);
15099        if (!self.hasWooCommerce()){
15100            console.log("woocommerce_params not found!");
15101            return;
15102        }
15103        var cls = self.tmpl.cls.woo;
15104        e.preventDefault();
15105        if ($this.hasClass(cls.disabled)){
15106            return false;
15107        }
15108        var productId = $this.attr("data-variation-id") || self.productId,
15109            quantity = $this.attr("data-quantity") || 1;
15110
15111        $this.removeClass(cls.added)
15112            .addClass(cls.adding)
15113            .addClass(cls.disabled);
15114
15115        self.addToCart($this, productId, quantity, true).then(function(){
15116            $this.removeClass(cls.adding)
15117                .removeClass(cls.disabled)
15118                .addClass(cls.added);
15119        });
15120        return false;
15121    };
15122
15123    _.Item.prototype.addToCart = function($button, productId, quantity, redirectOnError){
15124        var self = this,
15125            $body = $(document.body),
15126            fallback = "?add-to-cart=" + productId,
15127            data = [{
15128                "name": "product_id",
15129                "value": productId
15130            },{
15131                "name": "quantity",
15132                "value": quantity
15133            },{
15134                "name": "foogallery_attachment_id",
15135                "value": self.id
15136            },{
15137                "name": "foogallery_id",
15138                "value": self.tmpl.id
15139            }];
15140
15141        $body.trigger('adding_to_cart', [$button, data]);
15142        return $.ajax({
15143            type: 'POST',
15144            url: self.getWooCommerceEndPoint(),
15145            data: data
15146        }).then(function(response) {
15147            if (!response){
15148                console.log("An unexpected response was returned from the server.", response);
15149            } else if (response.error) {
15150                if (redirectOnError){
15151                    if (_is.string(response.product_url)){
15152                        window.location = response.product_url;
15153                    }
15154                    window.location = fallback;
15155                }
15156            } else {
15157                $body.trigger('added_to_cart', [response.fragments, response.cart_hash]);
15158            }
15159            return response;
15160        }, function(response, textStatus, errorThrown) {
15161            console.log("FooGallery: Add to cart ajax error.", response, textStatus, errorThrown);
15162            if (redirectOnError) {
15163                window.location = fallback;
15164            }
15165        });
15166    };
15167
15168    _.Item.override("doParseItem", function($el){
15169        var self = this;
15170        if (self._super($el)){
15171            $el.find(self.tmpl.sel.woo.button).on("click.foogallery", { self: self }, self.onAddToCart);
15172            return true
15173        }
15174        return false;
15175    });
15176
15177    _.Item.override("doCreateItem", function(){
15178        var self = this;
15179        if (self._super()){
15180            self.$el.find(self.tmpl.sel.woo.button).on("click.foogallery", { self: self }, self.onAddToCart);
15181            return true
15182        }
15183        return false;
15184    });
15185
15186    _.Item.override("doDestroyItem", function(){
15187        var self = this;
15188        if (self.isParsed) {
15189            self.$el.find(self.tmpl.sel.woo.button).off("click.foogallery");
15190        }
15191        return self._super();
15192    });
15193
15194})(
15195    FooGallery.$,
15196    FooGallery,
15197    FooGallery.utils.is
15198);
15199(function($, _, _utils){
15200
15201	_.DefaultTemplate = _.Template.extend({});
15202
15203	_.template.register("default", _.DefaultTemplate, null, {
15204		container: "foogallery fg-default"
15205	});
15206
15207})(
15208	FooGallery.$,
15209	FooGallery,
15210	FooGallery.utils
15211);
15212(function($, _, _utils, _is){
15213
15214	/**
15215	 * @summary The Masonry template for FooGallery.
15216	 * @memberof FooGallery.
15217	 * @constructs MasonryTemplate
15218	 * @param {Object} [options] - The options for the template.
15219	 * @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.
15220	 * @augments FooGallery.Template
15221	 * @borrows FooGallery.utils.Class.extend as extend
15222	 * @borrows FooGallery.utils.Class.override as override
15223	 * @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.
15224	 */
15225	_.MasonryTemplate = _.Template.extend(/** @lends FooGallery.MasonryTemplate */{
15226		construct: function(options, element){
15227			var self = this;
15228			self._super(options, element);
15229			self.masonry = null;
15230			self.on({
15231				"pre-init": self.onPreInit,
15232				"destroyed": self.onDestroyed,
15233				"appended-items": self.onAppendedItems,
15234				"detach-item": self.onDetachItem,
15235				"layout after-filter-change": self.onLayoutRequired,
15236				"page-change": self.onPageChange
15237			}, self);
15238		},
15239		onPreInit: function(){
15240			var self = this, sel = self.sel,
15241				layout = self.template.layout,
15242				fixed;
15243
15244			if (["fixed", "col1", "col2", "col3", "col4", "col5", "col6"].indexOf(layout) !== -1){
15245				self.$el.removeClass("fg-fixed fg-col1 fg-col2 fg-col3 fg-col4 fg-col5 fg-col6").addClass("fg-" + layout);
15246			}
15247			fixed = self.$el.hasClass("fg-fixed");
15248
15249			self.template.isFitWidth = fixed;
15250			self.template.percentPosition = !fixed;
15251			self.template.transitionDuration = 0;
15252			self.template.itemSelector = sel.item.elem;
15253			if (!fixed){
15254				self.template.gutter = sel.gutterWidth;
15255				self.template.columnWidth = sel.columnWidth;
15256			}
15257			self.masonry = new Masonry( self.el, self.template );
15258		},
15259		onDestroyed: function(){
15260			var self = this;
15261			if (self.masonry instanceof Masonry){
15262				self.masonry.destroy();
15263			}
15264		},
15265		onLayoutRequired: function(){
15266			this.masonry.layout();
15267		},
15268		onPageChange: function(event, current, prev, isFilter){
15269			if (!isFilter){
15270				this.masonry.layout();
15271			}
15272		},
15273		onAppendedItems: function(event, items){
15274			var self = this,
15275				elements = items.map(function(item){
15276					return item.el;
15277				}),
15278				mItems = self.masonry.addItems(elements);
15279			// add and layout the new items with no transitions
15280			self.masonry.layoutItems(mItems, true);
15281		},
15282		onDetachItem: function(event, item){
15283			if (!event.isDefaultPrevented()){
15284				event.preventDefault();
15285				this.masonry.remove(item.el);
15286				item.$el.removeClass(this.cls.hidden);
15287				item.isAttached = false;
15288			}
15289		}
15290	});
15291
15292	_.template.register("masonry", _.MasonryTemplate, {
15293		template: {}
15294	}, {
15295		container: "foogallery fg-masonry",
15296		columnWidth: "fg-column-width",
15297		gutterWidth: "fg-gutter-width"
15298	});
15299
15300})(
15301		FooGallery.$,
15302		FooGallery,
15303		FooGallery.utils,
15304		FooGallery.utils.is
15305);
15306
15307(function($, _, _utils, _is){
15308
15309	_.Justified = _utils.Class.extend({
15310		construct: function(template, options){
15311			var self = this;
15312			self.tmpl = template;
15313			self.$el = template.$el;
15314			self.options = $.extend(true, {}, _.Justified.defaults, options);
15315			self._items = [];
15316			self.maxRowHeight = 0;
15317			self.borderSize = 0;
15318			self.align = ["left","center","right"].indexOf(self.options.align) !== -1 ? self.options.align : "center";
15319		},
15320		init: function(){
15321			var self = this;
15322			self.maxRowHeight = self.getMaxRowHeight(self.options.maxRowHeight, self.options.rowHeight);
15323			self.borderSize = self.getBorderSize();
15324		},
15325		destroy: function(){
15326			this.$el.removeAttr("style");
15327		},
15328		getBorderSize: function(){
15329			var border = this.tmpl.getCSSClass("border", "");
15330			switch (border){
15331				case "fg-border-thin":
15332					return 4;
15333				case "fg-border-medium":
15334					return 10;
15335				case "fg-border-thick":
15336					return 16;
15337				default:
15338					return 0;
15339			}
15340		},
15341		getMaxRowHeight: function(value, def) {
15342			if (_is.string(value)){
15343				var parsed = parseInt(value);
15344				if (isNaN(parsed)) return def;
15345				if (parsed <= 0) return Infinity;
15346				return value.indexOf('%') !== -1 ? def * (parsed / 100) : parsed;
15347			}
15348			if (_is.number(value)){
15349				if (value <= 0) return Infinity;
15350				return value;
15351			}
15352			return def;
15353		},
15354		layout: function(width){
15355			var self = this;
15356			if (!_is.number(width)){
15357				width = self.$el.width();
15358			}
15359			if (width > 0){
15360				var result = self.createRows(width);
15361				if (result.height !== 0 && result.rows.length > 0){
15362					self.$el.height(result.height);
15363					result.rows.forEach(function(row, i){
15364						self.render(row, i === result.rows.length - 1);
15365					});
15366				}
15367			}
15368		},
15369		render: function(row, isLast){
15370			var self = this, applyMaxHeight = !isLast && self.options.lastRow !== "justify";
15371			row.items.forEach(function(item){
15372				if (item.elem){
15373					if (row.visible){
15374						item.elem.style.setProperty("position", "absolute");
15375						item.elem.style.setProperty("width", item.width + "px");
15376						item.elem.style.setProperty("height", item.height + "px");
15377						item.elem.style.setProperty("top", item.top + "px");
15378						item.elem.style.setProperty("left", item.left + "px");
15379						item.elem.style.setProperty("margin", "0");
15380						item.elem.style.removeProperty("display");
15381						if (self.maxRowHeight > 0 && applyMaxHeight){
15382							item.elem.style.setProperty("max-height", (self.maxRowHeight + (self.borderSize * 2)) + "px");
15383						} else {
15384							item.elem.style.removeProperty("max-height");
15385						}
15386						if (!item.elem.classList.contains("fg-positioned")){
15387							item.elem.classList.add("fg-positioned");
15388						}
15389					} else {
15390						item.elem.style.setProperty("display", "none");
15391					}
15392				}
15393			});
15394		},
15395		justify: function(row, top, maxWidth, maxHeight){
15396			var self = this,
15397				margin = self.options.margins,
15398				margins = margin * (row.items.length - 1),
15399				max = maxWidth - margins,
15400				rowWidth = row.width - margins;
15401
15402			var w_ratio = max / rowWidth;
15403			row.width = rowWidth * w_ratio;
15404			row.height = row.height * w_ratio;
15405
15406			if (row.height > (maxHeight + (self.borderSize * 2))){
15407				var h_ratio = (maxHeight + (self.borderSize * 2)) / row.height;
15408				row.width = row.width * h_ratio;
15409				row.height = row.height * h_ratio;
15410			}
15411
15412			row.top = top;
15413			// default is left 0 because a full row starts at 0 and it matches default layouts
15414			row.left = 0;
15415			// if we don't have a full row and align !== left
15416			if (self.align !== "left" && row.width < max){
15417				if (self.align === "right"){
15418					row.left = max - row.width;
15419				} else {
15420					row.left = (max - row.width) / 2;
15421				}
15422			}
15423
15424			row.width += margins;
15425
15426			var left = row.left;
15427			row.items.forEach(function(item, i){
15428				if (i > 0) left += margin;
15429				item.left = left;
15430				item.top = top;
15431				var i_ratio = row.height / item.height;
15432				item.width = item.width * i_ratio;
15433				item.height = item.height * i_ratio;
15434				left += item.width;
15435			});
15436
15437			return row.height;
15438		},
15439		createRows: function(maxWidth){
15440			var self = this,
15441				margin = self.options.margins,
15442				items = self.tmpl.getAvailable(),
15443				rows = [],
15444				index = -1;
15445
15446			function newRow(){
15447				return {
15448					index: ++index,
15449					visible: true,
15450					width: 0,
15451					height: self.options.rowHeight + (self.borderSize * 2),
15452					top: 0,
15453					left: 0,
15454					items: []
15455				};
15456			}
15457
15458			function newItem(item, rowHeight){
15459				var width = item.width, height = item.height;
15460				// make the item match the row height
15461				if (height !== rowHeight){
15462					var ratio = rowHeight / height;
15463					height = height * ratio;
15464					width = width * ratio;
15465				}
15466				var maxRatio = self.maxRowHeight / rowHeight,
15467					maxWidth = width * maxRatio,
15468					maxHeight = height * maxRatio;
15469				return {
15470					__item: item,
15471					elem: item.el,
15472					width: width,
15473					height: height,
15474					maxWidth: maxWidth,
15475					maxHeight: maxHeight,
15476					top: 0,
15477					left: 0
15478				};
15479			}
15480
15481			var row = newRow(), top = 0, max = 0;
15482			items.forEach(function(fgItem){
15483				var item = newItem(fgItem, row.height);
15484				// adding this item to the row would exceed the max width
15485				if (row.width + item.width > maxWidth && row.items.length > 0){
15486					if (rows.length > 0) top += margin;
15487					var height = self.justify(row, top, maxWidth, self.maxRowHeight); // first justify the current row
15488					if (height > max) max = height;
15489					top += height;
15490					rows.push(row);
15491					row = newRow(); // then make the new one
15492				}
15493
15494				if (row.items.length > 0) row.width += margin;
15495				row.width += item.width;
15496				row.items.push(item);
15497			});
15498
15499			if (row.items.length > 0){
15500				var height, top_start = top + margin;
15501				switch (self.options.lastRow){
15502					case "smart":
15503						height = self.justify(row, top_start, maxWidth, self.maxRowHeight);
15504						if (max !== 0 && height > max){
15505							var h_ratio = max / height,
15506								w_ratio = (row.width * h_ratio) / maxWidth;
15507
15508							if (h_ratio < 0.9 || w_ratio < 0.9){
15509								height = self.justify(row, top_start, maxWidth, max - (self.borderSize * 2));
15510							}
15511						}
15512						break;
15513					case "justify":
15514						height = self.justify(row, top_start, maxWidth, 99999);
15515						break;
15516					case "hide":
15517						height = self.justify(row, top_start, maxWidth, self.maxRowHeight);
15518						if (row.width < maxWidth && rows.length > 1){
15519							row.visible = false;
15520						}
15521						break;
15522				}
15523				if (row.visible){
15524					top += height + margin;
15525				}
15526				rows.push(row);
15527			}
15528
15529			return {
15530				height: top,
15531				rows: rows
15532			};
15533		}
15534	});
15535
15536	_.Justified.defaults = {
15537		rowHeight: 150,
15538		maxRowHeight: "200%",
15539		margins: 0,
15540		align: "center",
15541		lastRow: "smart" // "smart","justify","hide"
15542	};
15543
15544})(
15545		FooGallery.$,
15546		FooGallery,
15547		FooGallery.utils,
15548		FooGallery.utils.is
15549);
15550(function($, _){
15551
15552	_.JustifiedTemplate = _.Template.extend({
15553		construct: function(options, element){
15554			var self = this;
15555			self._super(options, element);
15556			self.justified = null;
15557			self.on({
15558				"pre-init": self.onPreInit,
15559				"init": self.onInit,
15560				"destroyed": self.onDestroyed,
15561				"layout after-filter-change": self.onLayoutRequired,
15562				"page-change": self.onPageChange
15563			}, self);
15564		},
15565		onPreInit: function(){
15566			var self = this;
15567			self.justified = new _.Justified( self, self.template );
15568		},
15569		onInit: function(){
15570			this.justified.init();
15571		},
15572		onDestroyed: function(){
15573			var self = this;
15574			if (self.justified instanceof _.Justified){
15575				self.justified.destroy();
15576			}
15577		},
15578		onLayoutRequired: function(){
15579			this.justified.layout(this.lastWidth);
15580		},
15581		onPageChange: function(event, current, prev, isFilter){
15582			if (!isFilter){
15583				this.justified.layout(this.lastWidth);
15584			}
15585		}
15586	});
15587
15588	_.template.register("justified", _.JustifiedTemplate, null, {
15589		container: "foogallery fg-justified"
15590	});
15591
15592})(
15593	FooGallery.$,
15594	FooGallery
15595);
15596(function($, _, _utils, _is, _fn){
15597
15598	_.PortfolioTemplate = _.Template.extend({});
15599
15600	_.template.register("simple_portfolio", _.PortfolioTemplate, {}, {
15601		container: "foogallery fg-simple_portfolio"
15602	});
15603
15604})(
15605		FooGallery.$,
15606		FooGallery,
15607	FooGallery.utils,
15608	FooGallery.utils.is,
15609	FooGallery.utils.fn
15610);
15611(function ($, _, _utils, _obj) {
15612
15613	_.ImageViewerTemplate = _.Template.extend({
15614		construct: function (options, element) {
15615			var self = this,
15616				dotsPosition = options && options.template ? options.template.dotsPosition : null,
15617                overlay = element.hasClass('fg-overlay-controls');
15618			if (["fg-dots-none", "fg-dots-left", "fg-dots-center", "fg-dots-right"].indexOf(dotsPosition) !== -1){
15619				element.removeClass("fg-dots-none fg-dots-left fg-dots-center fg-dots-right").addClass(dotsPosition);
15620			}
15621            if ( overlay && _.paging?.contains('dots') ) {
15622                var reg = _.paging.registered['dots'];
15623                options = _obj.extend({
15624                    cls: {
15625                        paging: reg.cls
15626                    },
15627                    il8n: {
15628                        paging: reg.il8n
15629                    }
15630                }, options, {
15631                    paging: {
15632                        pushOrReplace: "replace",
15633                        type: "dots",
15634                        size: 1,
15635                        position: element.hasClass('fg-dots-none') ? "none" : "bottom",
15636                        scrollToTop: false
15637                    }
15638                })
15639            } else {
15640                options = _obj.extend({}, options, {
15641                    paging: {
15642                        pushOrReplace: "replace",
15643                        type: "default",
15644                        size: 1,
15645                        position: "none",
15646                        scrollToTop: false
15647                    }
15648                });
15649            }
15650			self._super(options, element);
15651			/**
15652			 * @summary The jQuery object containing the inner element that wraps all items.
15653			 * @memberof FooGallery.ImageViewerTemplate#
15654			 * @name $inner
15655			 * @type {jQuery}
15656			 */
15657			self.$inner = $();
15658			/**
15659			 * @summary The jQuery object that displays the current image count.
15660			 * @memberof FooGallery.ImageViewerTemplate#
15661			 * @name $current
15662			 * @type {jQuery}
15663			 */
15664			self.$current = $();
15665			/**
15666			 * @summary The jQuery object that displays the current image count.
15667			 * @memberof FooGallery.ImageViewerTemplate#
15668			 * @name $current
15669			 * @type {jQuery}
15670			 */
15671			self.$total = $();
15672			self._autoplayTimeout = null;
15673			self._autoplayPaused = false;
15674			/**
15675			 * @summary The jQuery object for the previous button.
15676			 * @memberof FooGallery.ImageViewerTemplate#
15677			 * @name $prev
15678			 * @type {jQuery}
15679			 */
15680			self.$prev = $();
15681			/**
15682			 * @summary The jQuery object for the next button.
15683			 * @memberof FooGallery.ImageViewerTemplate#
15684			 * @name $next
15685			 * @type {jQuery}
15686			 */
15687			self.$next = $();
15688			/**
15689			 * @summary The CSS classes for the Image Viewer template.
15690			 * @memberof FooGallery.ImageViewerTemplate#
15691			 * @name cls
15692			 * @type {FooGallery.ImageViewerTemplate~CSSClasses}
15693			 */
15694			/**
15695			 * @summary The CSS selectors for the Image Viewer template.
15696			 * @memberof FooGallery.ImageViewerTemplate#
15697			 * @name sel
15698			 * @type {FooGallery.ImageViewerTemplate~CSSSelectors}
15699			 */
15700			self.on({
15701				"loaded-item error-item": self.onFirstItemReady,
15702				"pre-init": self.onPreInit,
15703				"init": self.onInit,
15704				"ready": self.onReady,
15705				"destroy": self.onDestroy,
15706				"append-item": self.onAppendItem,
15707				"after-page-change": self.onAfterPageChange,
15708				"after-filter-change": self.onAfterFilterChange
15709			}, self);
15710		},
15711		createChildren: function(){
15712			var self = this;
15713			return $("<div/>", {"class": self.cls.inner}).append(
15714					$("<div/>", {"class": self.cls.innerContainer}),
15715					$("<div/>", {"class": self.cls.controls}).append(
15716							$("<button/>", {"class": self.cls.prev, type: 'button', title: self.il8n.prev})
15717									.append($("<span/>", {text: self.il8n.prev})),
15718							$("<label/>", {"class": self.cls.count, text: self.il8n.count})
15719									.prepend($("<span/>", {"class": self.cls.countCurrent, text: "0"}))
15720									.append($("<span/>", {"class": self.cls.countTotal, text: "0"})),
15721							$("<button/>", {"class": self.cls.next, type: 'button', title: self.il8n.next})
15722									.append($("<span/>", {text: self.il8n.next}))
15723					)
15724			);
15725		},
15726		destroyChildren: function(){
15727			var self = this;
15728			self.$el.find(self.sel.inner).remove();
15729		},
15730        onFirstItemReady: function(event) {
15731            this.$el.removeClass("fg-not-ready");
15732        },
15733
15734		onPreInit: function(event){
15735			var self = this;
15736            self.$el.addClass("fg-not-ready");
15737			self.$inner = self.$el.find(self.sel.innerContainer);
15738			self.$current = self.$el.find(self.sel.countCurrent);
15739			self.$total = self.$el.find(self.sel.countTotal);
15740			self.$prev = self.$el.find(self.sel.prev);
15741            if ( !self.$prev.attr('title') ) {
15742                self.$prev.attr('title', self.il8n.prev);
15743            }
15744			self.$next = self.$el.find(self.sel.next);
15745            if ( !self.$next.attr('title') ) {
15746                self.$next.attr('title', self.il8n.next);
15747            }
15748		},
15749		onInit: function (event) {
15750			var self = this;
15751			if (self.template.attachFooBox) {
15752				self.$el.on('foobox.previous', {self: self}, self.onFooBoxPrev)
15753						.on('foobox.next', {self: self}, self.onFooBoxNext);
15754			}
15755			self.$prev.on('click', {self: self}, self.onPrevClick);
15756			self.$next.on('click', {self: self}, self.onNextClick);
15757			self.$el.on('mouseenter.foogallery-autoplay', {self: self}, self.onAutoplayMouseEnter)
15758					.on('mouseleave.foogallery-autoplay', {self: self}, self.onAutoplayMouseLeave);
15759		},
15760		onReady: function (event) {
15761			this.update();
15762			this.startAutoplay();
15763		},
15764		/**
15765		 * @summary Destroy the plugin cleaning up any bound events.
15766		 * @memberof FooGallery.ImageViewerTemplate#
15767		 * @function onDestroy
15768		 */
15769		onDestroy: function (event) {
15770			var self = this;
15771			if (self.template.attachFooBox) {
15772				self.$el.off({
15773					'foobox.previous': self.onFooBoxPrev,
15774					'foobox.next': self.onFooBoxNext
15775				});
15776			}
15777			self.$prev.off('click', self.onPrevClick);
15778			self.$next.off('click', self.onNextClick);
15779			self.$el.off('.foogallery-autoplay');
15780			self.clearAutoplay();
15781		},
15782		onAppendItem: function (event, item) {
15783			event.preventDefault();
15784			this.$inner.append(item.$el);
15785			item.isAttached = true;
15786		},
15787		onAfterPageChange: function(event, current, prev, isFilter){
15788			if (!isFilter){
15789				this.update();
15790				this.startAutoplay();
15791			}
15792		},
15793		onAfterFilterChange: function(event){
15794			this.update();
15795			this.startAutoplay();
15796		},
15797
15798		update: function(){
15799			if (this.pages){
15800				this.$current.text(this.pages.current);
15801				this.$total.text(this.pages.total);
15802			}
15803		},
15804		getAutoplaySeconds: function(){
15805			var seconds = parseInt(this.template.autoplay, 10);
15806			return isNaN(seconds) ? 0 : seconds;
15807		},
15808		clearAutoplay: function(){
15809			if (this._autoplayTimeout !== null){
15810				clearTimeout(this._autoplayTimeout);
15811				this._autoplayTimeout = null;
15812			}
15813		},
15814		startAutoplay: function(){
15815			var self = this,
15816				seconds = self.getAutoplaySeconds();
15817
15818			self.clearAutoplay();
15819
15820			if (self._autoplayPaused || seconds <= 0 || !self.pages || self.pages.total <= 1){
15821				return;
15822			}
15823
15824			if (!self.template.loop && self.pages.current >= self.pages.total){
15825				return;
15826			}
15827
15828			self._autoplayTimeout = setTimeout(function(){
15829				self._autoplayTimeout = null;
15830				self.next();
15831			}, seconds * 1000);
15832		},
15833		onAutoplayMouseEnter: function(e){
15834			e.data.self._autoplayPaused = true;
15835			e.data.self.clearAutoplay();
15836		},
15837		onAutoplayMouseLeave: function(e){
15838			e.data.self._autoplayPaused = false;
15839			e.data.self.startAutoplay();
15840		},
15841		/**
15842		 * @summary Navigate to the previous item in the collection.
15843		 * @memberof FooGallery.ImageViewerTemplate#
15844		 * @function prev
15845		 * @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.
15846		 */
15847		prev: function () {
15848			if (this.pages){
15849				if (this.template.loop && this.pages.current === 1){
15850					this.pages.last();
15851				} else {
15852					this.pages.prev();
15853				}
15854				this.update();
15855			}
15856		},
15857		/**
15858		 * @summary Navigate to the next item in the collection.
15859		 * @memberof FooGallery.ImageViewerTemplate#
15860		 * @function next
15861		 * @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.
15862		 */
15863		next: function () {
15864			if (this.pages){
15865				if (this.template.loop && this.pages.current === this.pages.total){
15866					this.pages.first();
15867				} else {
15868					this.pages.next();
15869				}
15870				this.update();
15871			}
15872		},
15873		/**
15874		 * @summary Handles the `"foobox.previous"` event allowing the plugin to remain in sync with what is displayed in the lightbox.
15875		 * @memberof FooGallery.ImageViewerTemplate#
15876		 * @function onFooBoxPrev
15877		 * @param {jQuery.Event} e - The jQuery.Event object for the event.
15878		 */
15879		onFooBoxPrev: function (e) {
15880			e.data.self.prev();
15881		},
15882		/**
15883		 * @summary Handles the `"foobox.next"` event allowing the plugin to remain in sync with what is displayed in the lightbox.
15884		 * @memberof FooGallery.ImageViewerTemplate#
15885		 * @function onFooBoxNext
15886		 * @param {jQuery.Event} e - The jQuery.Event object for the event.
15887		 */
15888		onFooBoxNext: function (e) {
15889			e.data.self.next();
15890		},
15891		/**
15892		 * @summary Handles the `"click"` event of the previous button.
15893		 * @memberof FooGallery.ImageViewerTemplate#
15894		 * @function onPrevClick
15895		 * @param {jQuery.Event} e - The jQuery.Event object for the event.
15896		 */
15897		onPrevClick: function (e) {
15898			e.preventDefault();
15899			e.stopPropagation();
15900			e.data.self.prev();
15901		},
15902		/**
15903		 * @summary Handles the `"click"` event of the next button.
15904		 * @memberof FooGallery.ImageViewerTemplate#
15905		 * @function onNextClick
15906		 * @param {jQuery.Event} e - The jQuery.Event object for the event.
15907		 */
15908		onNextClick: function (e) {
15909			e.preventDefault();
15910			e.stopPropagation();
15911			e.data.self.next();
15912		}
15913	});
15914
15915	_.template.register("image-viewer", _.ImageViewerTemplate, {
15916		template: {
15917			attachFooBox: false,
15918			loop: false,
15919			autoplay: 0
15920		}
15921	}, {
15922		container: "foogallery fg-image-viewer",
15923		inner: "fiv-inner",
15924		innerContainer: "fiv-inner-container",
15925		controls: "fiv-ctrls",
15926		prev: "fiv-prev",
15927		next: "fiv-next",
15928		count: "fiv-count",
15929		countCurrent: "fiv-count-current",
15930		countTotal: "fiv-count-total"
15931	}, {
15932		prev: "Prev",
15933		next: "Next",
15934		count: "of"
15935	});
15936
15937})(
15938		FooGallery.$,
15939		FooGallery,
15940		FooGallery.utils,
15941		FooGallery.utils.obj
15942);
15943
15944(function($, _, _obj){
15945
15946	_.ThumbnailTemplate = _.Template.extend({
15947		construct: function (options, element) {
15948			this._super(_obj.extend({}, options, {
15949				filtering: {
15950					type: "none"
15951				},
15952				paging: {
15953					pushOrReplace: "replace",
15954					theme: "fg-light",
15955					type: "default",
15956					size: $(element).hasClass((options.cls.stacked)) ? 3 : 1,
15957					position: "none",
15958					scrollToTop: false
15959				}
15960			}), element);
15961		}
15962	});
15963
15964	_.template.register("thumbnail", _.ThumbnailTemplate, {
15965        template: {}
15966    }, {
15967		container: "foogallery fg-thumbnail",
15968        stacked: "fg-stacked"
15969	});
15970
15971})(
15972		FooGallery.$,
15973		FooGallery,
15974		FooGallery.utils.obj
15975);
15976(function($, _, _icons, _utils, _is){
15977
15978    _utils.Progress = _utils.EventClass.extend( {
15979        construct: function( tickRate ){
15980            const self = this;
15981            self._super();
15982            self.percent = 0;
15983            self.tickRate = _is.number( tickRate ) ? tickRate : 100;
15984            self.isPaused = false;
15985            self.isActive = false;
15986            self._intervalId = null;
15987            self._total = 0;
15988            self._target = null;
15989            self.onTick = self.onTick.bind( self );
15990        },
15991        destroy: function(){
15992            const self = this;
15993            self._reset();
15994            self._super();
15995        },
15996        _reset: function(){
15997            const self = this;
15998            if ( self._intervalId !== null ) clearInterval( self._intervalId );
15999            self.percent = 0;
16000            self._total = 0;
16001            self._intervalId = null;
16002            self._target = null;
16003            self.isActive = false;
16004            self.isPaused = false;
16005        },
16006        stop: function(){
16007            const self = this;
16008            if ( self.isActive ){
16009                self._reset();
16010                self.trigger( "stop" );
16011            }
16012        },
16013        start: function( seconds ){
16014            const self = this;
16015            self.stop();
16016            if ( !self.isActive && _is.number( seconds ) && seconds > 0 ){
16017                self._total = seconds * 1000;
16018                self._target = Date.now() + self._total;
16019                self._intervalId = setInterval( self.onTick, self.tickRate );
16020                self.isActive = true;
16021                self.trigger( "start" );
16022            }
16023        },
16024        pause: function(){
16025            const self = this;
16026            if ( self.isActive && !self.isPaused && self.percent < 100 ){
16027                if ( self._intervalId !== null ) clearInterval( self._intervalId );
16028                self._intervalId = null;
16029                self._target = null;
16030                self.isPaused = true;
16031                self.trigger( "pause" );
16032            }
16033        },
16034        resume: function(){
16035            const self = this;
16036            if ( self.isActive && self.isPaused ){
16037                const remaining = self._total - ( ( self._total * self.percent ) / 100 );
16038                self._target = Date.now() + remaining;
16039                self._intervalId = setInterval( self.onTick, self.tickRate );
16040                self.isPaused = false;
16041                self.trigger( "resume" );
16042            } else if ( self._total > 0 ){
16043                self.start( self._total / 1000 );
16044            }
16045        },
16046        onTick: function(){
16047            const self = this;
16048            const diff = self._total - Math.max( self._target - Date.now(), 0 );
16049            self.percent = Math.min( ( diff / self._total ) * 100, 100 );
16050            self.trigger( "tick", [ self.percent ] );
16051            if ( self.percent >= 100 ){
16052                if ( self._intervalId !== null ) clearInterval( self._intervalId );
16053                self._intervalId = null;
16054                self._target = null;
16055                self.isActive = false;
16056                self.trigger( "complete" );
16057            }
16058        }
16059    } );
16060
16061    /**
16062     * @memberof FooGallery.
16063     * @class Carousel
16064     * @extends FooGallery.utils.Class
16065     */
16066    _.Carousel = _utils.Class.extend( /** @lends FooGallery.Carousel.prototype */ {
16067        /**
16068         * @ignore
16069         * @constructs
16070         * @param {FooGallery.Template} template
16071         * @param {object} opt
16072         * @param {object} cls
16073         * @param {object} sel
16074         */
16075        construct: function(template, opt, cls, sel, i18n){
16076            const self = this;
16077            self.tmpl = template;
16078            self.el = template.el;
16079            self.opt = opt;
16080            self.cls = cls;
16081            self.sel = sel;
16082            self.i18n = i18n;
16083            self.elem = {
16084                inner: null,
16085                center: null,
16086                bottom: null,
16087                prev: null,
16088                next: null,
16089                progress: null
16090            };
16091            self.activeItem = null;
16092            self._itemWidth = 0;
16093            self._leftExclude = [ self.sel.activeItem, self.sel.nextItem ].join( "," );
16094            self._rightExclude = [ self.sel.activeItem, self.sel.prevItem ].join( "," );
16095            /**
16096             *
16097             * @type {FooGallery.utils.DOMEventListeners}
16098             * @private
16099             */
16100            self._centerListeners = new _utils.DOMEventListeners();
16101            /**
16102             *
16103             * @type {FooGallery.utils.DOMEventListeners}
16104             * @private
16105             */
16106            self._listeners = new _utils.DOMEventListeners();
16107            self._generatedControls = [];
16108            /**
16109             *
16110             * @type {FooGallery.utils.Progress}
16111             * @private
16112             */
16113            self._progress = new _utils.Progress();
16114            self._canHover = window.matchMedia("(hover: hover)").matches;
16115            self.cache = new Map();
16116            self.timeouts = new _utils.Timeouts();
16117            self.interacted = false;
16118            self.isRTL = false;
16119            self._firstLayout = true;
16120        },
16121
16122        //#region Transform Utils
16123
16124        /**
16125         * @summary Calculate the screen X co-ord given a 3D points' X, Z co-ords and the perspective.
16126         * @memberof
16127         * @param {number} vectorX - The 3D point X co-ord.
16128         * @param {number} vectorZ - The 3D point Z co-ord.
16129         * @param {number} perspective - The visual perspective.
16130         * @returns {number} The screen X co-ord.
16131         */
16132        getScreenX: function( vectorX, vectorZ, perspective ){
16133            return ( perspective / ( perspective + vectorZ ) ) * vectorX;
16134        },
16135        /**
16136         * @summary Calculate a 3D points' X co-ord given the screens' X co-ord, the 3D points' Z co-ord and the perspective.
16137         * @param {number} screenX - The screen X co-ord.
16138         * @param {number} vectorZ - The 3D point Z co-ord.
16139         * @param {number} perspective - The visual perspective.
16140         * @returns {number} The 3D point X co-ord.
16141         */
16142        getVectorX: function( screenX, vectorZ, perspective ){
16143            return ( ( perspective + vectorZ ) / perspective ) * screenX;
16144        },
16145        /**
16146         * @summary Calculate the incremental Z co-ord for a 3D point that is part of a scaled sequence.
16147         * @param {number} index - The index within the sequence for this iteration.
16148         * @param {number} scale - The scale to adjust each iteration by.
16149         * @param {number} perspective - The visual perspective.
16150         * @returns {number} The 3D point Z co-ord
16151         */
16152        getSequentialZFromScale: function( index, scale, perspective ){
16153            return perspective * ( scale * ( index + 1 ) );
16154        },
16155        /**
16156         * @summary Scales the value to the given 3D points' Z co-ord and perspective.
16157         * @param {number} value - The value to be scaled.
16158         * @param {number} vectorZ - The 3D point Z co-ord.
16159         * @param {number} perspective - The visual perspective.
16160         * @returns {number} The scaled value.
16161         */
16162        scaleToZ: function( value, vectorZ, perspective ){
16163            return value * ( 1 - vectorZ / ( perspective + vectorZ ) );
16164        },
16165        /**
16166         * @summary Returns the absolute difference between 2 numbers.
16167         * @param {number} num1
16168         * @param {number} num2
16169         * @returns {number}
16170         */
16171        getDiff: function( num1, num2 ){
16172            return num1 > num2 ? num1 - num2 : num2 - num1;
16173        },
16174
16175        //#endregion
16176
16177        //#region Autoplay
16178
16179        pause: function(){
16180            this._progress.pause();
16181        },
16182        resume: function(){
16183            this._progress.resume();
16184        },
16185        start: function(){
16186            if ( this.opt.autoplay.interaction === "disable" && this.interacted ) return;
16187            this._progress.start( this.opt.autoplay.time );
16188        },
16189        stop: function(){
16190            this._progress.stop();
16191        },
16192
16193        //#endregion
16194
16195        init: function(){
16196            const self = this;
16197            self.isRTL = window.getComputedStyle(self.el).direction === "rtl";
16198
16199            self.elem = {
16200                inner: self.el.querySelector( self.sel.inner ),
16201                center: self.el.querySelector( self.sel.center ),
16202                bottom: self.el.querySelector( self.sel.bottom ),
16203                prev: self.el.querySelector( self.sel.prev ),
16204                next: self.el.querySelector( self.sel.next ),
16205                progress: self.el.querySelector( self.sel.progress )
16206            };
16207
16208            if ( self.opt.perspective !== 150 ){
16209                self.el.style.setProperty( "--fg-carousel-perspective", self.opt.perspective + "px" );
16210            }
16211        },
16212        postInit: function(){
16213            const self = this;
16214            self.activeItem = self.tmpl.items.first();
16215            self.initNavigation();
16216            self.initPagination();
16217            self.initSwipe();
16218            self.initAutoplay();
16219        },
16220        initNavigation: function(){
16221            const self = this;
16222            self._listeners.add( self.elem.prev, "click", function( event ){
16223                event.preventDefault();
16224                self.interacted = true;
16225                self.previous();
16226            } );
16227            if ( self.elem.prev.type !== "button" ) self.elem.prev.type = "button";
16228            const prevIcon = _icons.element( "arrow-left" );
16229            self._generatedControls.push( prevIcon );
16230            self.elem.prev.appendChild( prevIcon );
16231            if ( !self.elem.prev.title?.length ) {
16232                self.elem.prev.title = self.i18n.prev;
16233            }
16234
16235            self._listeners.add( self.elem.next, "click", function( event ){
16236                event.preventDefault();
16237                self.interacted = true;
16238                self.next();
16239            } );
16240            if ( self.elem.next.type !== "button" ) self.elem.next.type = "button";
16241            const nextIcon = _icons.element( "arrow-right" );
16242            self._generatedControls.push( nextIcon );
16243            self.elem.next.appendChild( nextIcon );
16244            if ( !self.elem.next.title?.length ) {
16245                self.elem.next.title = self.i18n.next;
16246            }
16247        },
16248        getBulletTitle: function( str, num ) {
16249            return str.replace( /\{ITEM}/g, `${ num }` );
16250        },
16251        initPagination: function(){
16252            const self = this;
16253            const count = self.tmpl.items.count();
16254            for (let i = 0; i < count; i++){
16255                const bullet = document.createElement( "button" );
16256                bullet.type = "button";
16257                bullet.classList.add( self.cls.bullet );
16258                bullet.title = self.getBulletTitle( self.i18n.bullet, i + 1 );
16259                if ( i === 0 ){
16260                    bullet.classList.add( self.cls.activeBullet );
16261                    bullet.title = self.getBulletTitle( self.i18n.activeBullet, i + 1 );
16262                }
16263                self._listeners.add( bullet, "click", function( event ){
16264                    event.preventDefault();
16265                    self.interacted = true;
16266                    self.goto( self.tmpl.items.get( i ) );
16267                } );
16268                self._generatedControls.push( bullet );
16269                self.elem.bottom.appendChild( bullet );
16270            }
16271        },
16272        initSwipe: function(){
16273            const self = this;
16274            let startX = 0, endX = 0, min = 25 * (window.devicePixelRatio || 1);
16275            self._listeners.add( self.elem.inner, "touchstart", function( event ){
16276                self.interacted = true;
16277                startX = event.changedTouches[0].screenX;
16278            }, { passive: true } );
16279
16280            self._listeners.add( self.elem.inner, "touchend", function( event ){
16281                endX = event.changedTouches[0].screenX;
16282                const diff = self.getDiff( startX, endX );
16283                if ( diff > min ){
16284                    if ( endX < startX ){ // swipe left
16285                        self.next();
16286                    } else { // swipe right
16287                        self.previous();
16288                    }
16289                }
16290                endX = 0;
16291                startX = 0;
16292            }, { passive: true } );
16293        },
16294        initAutoplay: function(){
16295            const self = this, opt = self.opt.autoplay;
16296            if ( opt.time <= 0 || !( self.elem.progress instanceof HTMLElement ) ) return;
16297
16298            const progressElement = self.elem.progress.style;
16299
16300            let frame = null;
16301            function update( percent, immediate ){
16302                _utils.cancelFrame( frame );
16303                _utils.requestFrame( function(){
16304                    progressElement.setProperty( "width", percent + "%" );
16305                    if ( immediate ){
16306                        progressElement.setProperty( "transition-duration", "0s" );
16307                    } else {
16308                        progressElement.setProperty( "transition-duration", self._progress.tickRate + "ms" );
16309                    }
16310                } );
16311            }
16312
16313            self._progress.on( {
16314                "start stop": function(){
16315                    update( 0, true );
16316                },
16317                "pause resume": function(){
16318                    update( self._progress.percent, true );
16319                },
16320                "tick": function() {
16321                    update( self._progress.percent, false );
16322                },
16323                "complete": function() {
16324                    self.next( function(){
16325                        self._progress.start( opt.time );
16326                    } );
16327                }
16328            } );
16329
16330            if ( opt.interaction === "pause" ){
16331                if ( self._canHover ) {
16332                    self._listeners.add( self.el, "mouseenter", function( event ) {
16333                        self._progress.pause();
16334                    }, { passive: true } );
16335                    self._listeners.add( self.el, "mouseleave", function( event ) {
16336                        self._progress.resume();
16337                    }, { passive: true } );
16338                } else {
16339                    // handle touch only
16340                    self._listeners.add( self.el, "touchstart", function( event ) {
16341                        self.timeouts.delete( "autoplay" );
16342                        self._progress.pause();
16343                    }, { passive: true } );
16344
16345                    self._listeners.add( self.el, "touchend", function( event ) {
16346                        self.timeouts.set( "autoplay", function(){
16347                            self._progress.resume();
16348                        }, opt.time * 1000 );
16349                    }, { passive: true } );
16350                }
16351            }
16352            self._progress.start( opt.time );
16353        },
16354        getFirst: function(){
16355            return this.tmpl.items.first();
16356        },
16357        getNext: function( item ){
16358            return this.tmpl.items.next( !( item instanceof _.Item ) ? this.activeItem : item, null, true );
16359        },
16360        getPrev: function( item ){
16361            return this.tmpl.items.prev( !( item instanceof _.Item ) ? this.activeItem : item, null, true );
16362        },
16363        goto: function( item, callback ){
16364            const self = this;
16365            if ( !( item instanceof _.Item ) ){
16366                return;
16367            }
16368            const autoplay = self.opt.autoplay;
16369            const restart = !self._canHover && self.timeouts.has( "autoplay" );
16370            self.timeouts.delete( "autoplay" );
16371            self.timeouts.delete( "navigation" );
16372
16373            const pause = self._progress.isPaused;
16374            if ( self._progress.isActive ){
16375                self._progress.stop();
16376            }
16377
16378            self.activeItem = item;
16379            self.layout();
16380
16381            self.timeouts.set( "navigation", function(){
16382                if ( autoplay.time > 0 && ( autoplay.interaction === "pause" || ( autoplay.interaction === "disable" && !self.interacted ) ) ){
16383                    self._progress.start( autoplay.time );
16384                    if ( pause ){
16385                        self._progress.pause();
16386                    }
16387                    if ( restart ){
16388                        self.timeouts.set( "autoplay", function(){
16389                            self._progress.resume();
16390                        }, autoplay.time * 1000 );
16391                    }
16392                }
16393                if ( _is.fn( callback ) ) callback.call( self );
16394            }, self.opt.speed );
16395        },
16396        next: function( callback ){
16397            this.goto( this.getNext(), callback );
16398        },
16399        previous: function( callback ){
16400            this.goto( this.getPrev(), callback );
16401        },
16402        destroy: function(){
16403            const self = this;
16404            self.cache.clear();
16405            self.timeouts.clear();
16406            self._listeners.clear();
16407            self._centerListeners.clear();
16408            self._progress.destroy();
16409            // Breakpoint rebuilds reuse the markup, so remove this instance's controls.
16410            self._generatedControls.forEach( function( element ){
16411                element.remove();
16412            } );
16413            self._generatedControls = [];
16414            if ( self.opt.perspective !== 150 ){
16415                self.el.style.removeProperty( "--fg-carousel-perspective" );
16416            }
16417        },
16418        getSize: function( element, inner ){
16419            const rect = element.getBoundingClientRect();
16420            const size = { width: rect.width, height: rect.height };
16421            if ( inner ){
16422                const style = getComputedStyle( element );
16423                size.width -= parseFloat( style.paddingLeft ) + parseFloat( style.paddingRight ) + parseFloat( style.borderLeftWidth ) + parseFloat( style.borderRightWidth );
16424                size.height -= parseFloat( style.paddingTop ) + parseFloat( style.paddingBottom ) + parseFloat( style.borderTopWidth ) + parseFloat( style.borderBottomWidth );
16425            }
16426            return size;
16427        },
16428        layout: function( width ){
16429            const self = this;
16430            if ( self.activeItem === null ){
16431                self.activeItem = self.tmpl.items.first();
16432            }
16433            if ( !_is.number( width ) && self.cache.has( "width" ) ){
16434                width = self.cache.get( "width" );
16435            }
16436            if ( !_is.number( width ) ) return;
16437
16438            const layout = self.getLayout( width );
16439            if ( self._layoutFrame !== null ) _utils.cancelFrame( self._layoutFrame );
16440            self._layoutFrame = _utils.requestFrame( function(){
16441                self._layoutFrame = null;
16442                if ( self.renderActive( layout ) ){
16443                    self.renderSide( "left", self.sel.prevItem, self._leftExclude, self.cls.prevItem, layout );
16444                    self.renderSide( "right", self.sel.nextItem, self._rightExclude, self.cls.nextItem, layout );
16445                    self._firstLayout = false;
16446                }
16447            } );
16448        },
16449        getLayout: function( width ){
16450            const self = this;
16451            const activePosition = self.getActivePosition();
16452            const align = self.getAlign();
16453            const usePositionedLayout = activePosition !== "center";
16454            const mode = activePosition + "|" + align;
16455
16456            if ( self.cache.has("layout") && self.cache.get("width") === width && self.cache.get("layoutMode") === mode ){
16457                return self.cache.get("layout");
16458            }
16459            const itemWidth = self.getSize( self.elem.center ).width;
16460            const innerWidth = self.getSize( self.elem.inner, true ).width;
16461            const maxOffset = ( innerWidth / 2 ) + ( itemWidth / 2 );
16462            const layout = self.calculate( itemWidth, maxOffset );
16463
16464            layout.activeX = 0;
16465            layout.sideBySide = null;
16466
16467            if ( usePositionedLayout ) {
16468                const visualLeftSide = self.isRTL ? "right" : "left";
16469                const visualRightSide = self.isRTL ? "left" : "right";
16470                const dominantSide = activePosition === "start" ? visualRightSide : visualLeftSide;
16471                let showCount = Math.max( 0, self.getShowPerSide() * 2 );
16472                let positionedLayout = null;
16473                let sideLayout = [];
16474                let fit = { width: itemWidth, center: 0 };
16475
16476                // maxItems is a cap; reduce visible items until the positioned sequence fits inner width.
16477                while ( showCount >= 0 ){
16478                    positionedLayout = self.calculate(
16479                        itemWidth,
16480                        innerWidth + ( itemWidth / 2 ),
16481                        self.opt.gutter.max,
16482                        showCount
16483                    );
16484                    sideLayout = positionedLayout.side;
16485                    fit = self.measureAlignedFit( itemWidth, sideLayout, dominantSide );
16486                    if ( fit.width <= innerWidth ){
16487                        break;
16488                    }
16489                    showCount -= 1;
16490                }
16491
16492                // Keep the displayed sequence centered while the active item stays at the chosen position.
16493                layout.activeX = -fit.center;
16494                layout.sideBySide = {};
16495                layout.sideBySide.left = [];
16496                layout.sideBySide.right = [];
16497                layout.sideBySide[dominantSide] = sideLayout;
16498                if ( positionedLayout && _is.number( positionedLayout.zIndex ) ){
16499                    layout.zIndex = positionedLayout.zIndex;
16500                }
16501            }
16502
16503            self.applyVisualAlign( layout, itemWidth, innerWidth );
16504
16505            self.cache.set( "width", width );
16506            self.cache.set( "layoutMode", mode );
16507            if ( layout?.side?.length > 0 ) {
16508                self.cache.set( "layout", layout );
16509            }
16510            return layout;
16511        },
16512        getActivePosition: function(){
16513            const value = _is.string( this.opt.activePosition )
16514                ? this.opt.activePosition.toLowerCase()
16515                : "center";
16516            return [ "start", "end" ].indexOf( value ) !== -1 ? value : "center";
16517        },
16518        getAlign: function(){
16519            const value = _is.string( this.opt.align )
16520                ? this.opt.align.toLowerCase()
16521                : "center";
16522            return [ "left", "right" ].indexOf( value ) !== -1 ? value : "center";
16523        },
16524        applyVisualAlign: function( layout, itemWidth, innerWidth ){
16525            const self = this;
16526            const align = self.getAlign();
16527            if ( align === "center" ) return;
16528
16529            const bounds = self.measureVisibleBounds( itemWidth, layout );
16530            const target = align === "left"
16531                ? -( innerWidth / 2 )
16532                : ( innerWidth / 2 );
16533            const delta = align === "left"
16534                ? target - bounds.min
16535                : target - bounds.max;
16536
16537            layout.activeX = ( _is.number( layout.activeX ) ? layout.activeX : 0 ) + delta;
16538        },
16539        measureVisibleBounds: function( itemWidth, layout ){
16540            const self = this;
16541            const activeX = _is.number( layout?.activeX ) ? layout.activeX : 0;
16542            const leftValues = layout?.sideBySide?.left || layout.side || [];
16543            const rightValues = layout?.sideBySide?.right || layout.side || [];
16544            const result = {
16545                min: activeX - ( itemWidth / 2 ),
16546                max: activeX + ( itemWidth / 2 )
16547            };
16548            const perspective = self.opt.perspective;
16549            const update = function( side, values ){
16550                for ( let i = 0; i < values.length; i++ ){
16551                    const entry = values[i];
16552                    const signedX = ( side === "left" && !self.isRTL ) || ( side === "right" && self.isRTL ) ? -entry.x : entry.x;
16553                    const centerX = self.getScreenX( signedX, entry.z, perspective ) + activeX;
16554                    const width = self.scaleToZ( itemWidth, entry.z, perspective );
16555                    const min = centerX - ( width / 2 );
16556                    const max = centerX + ( width / 2 );
16557                    if ( min < result.min ) result.min = min;
16558                    if ( max > result.max ) result.max = max;
16559                }
16560            };
16561
16562            update( "left", leftValues );
16563            update( "right", rightValues );
16564
16565            return result;
16566        },
16567        measureAlignedFit: function( itemWidth, sideLayout, dominantSide ){
16568            const self = this;
16569            const result = {
16570                min: -( itemWidth / 2 ),
16571                max: itemWidth / 2,
16572                width: itemWidth,
16573                center: 0
16574            };
16575
16576            for ( let i = 0; i < sideLayout.length; i++ ){
16577                const values = sideLayout[i];
16578                const signedX = ( dominantSide === "left" && !self.isRTL ) || ( dominantSide === "right" && self.isRTL ) ? -values.x : values.x;
16579                const centerX = self.getScreenX( signedX, values.z, self.opt.perspective );
16580                const width = self.scaleToZ( itemWidth, values.z, self.opt.perspective );
16581                const left = centerX - ( width / 2 );
16582                const right = centerX + ( width / 2 );
16583                if ( left < result.min ) result.min = left;
16584                if ( right > result.max ) result.max = right;
16585            }
16586
16587            result.width = result.max - result.min;
16588            result.center = ( result.min + result.max ) / 2;
16589
16590            return result;
16591        },
16592        round: function( value, precision ){
16593            let multiplier = Math.pow(10, precision || 0);
16594            return Math.round(value * multiplier) / multiplier;
16595        },
16596        getShowPerSide: function(){
16597            const self = this, count = self.tmpl.items.count();
16598            let perSide = self.opt.maxItems;
16599            if ( perSide === "auto" || perSide <= 0 ) perSide = ( count % 2 === 0 ? count - 1 : count );
16600            if ( perSide % 2 === 0 ) perSide -= 1;
16601            if ( perSide < 1 ) perSide = 1;
16602            if ( perSide > count ) perSide = ( count % 2 === 0 ? count - 1 : count );
16603            return ( perSide - 1 ) / 2;
16604        },
16605        calculate: function( itemWidth, maxOffset, gutter, showPerSide ){
16606            const self = this;
16607            if ( !_is.number( gutter ) ){
16608                gutter = self.opt.gutter.max;
16609            }
16610            if ( !_is.number( showPerSide ) ){
16611                showPerSide = self.getShowPerSide();
16612            }
16613
16614            const result = {
16615                zIndex: showPerSide + 10,
16616                gutter: gutter,
16617                perSide: showPerSide,
16618                side: []
16619            };
16620
16621            if ( itemWidth > 0 ) {
16622                let offset = itemWidth, zIndex = result.zIndex - 1;
16623                for (let i = 0; i < showPerSide; i++, zIndex--){
16624                    const z = self.getSequentialZFromScale( i, self.opt.scale, self.opt.perspective );
16625                    const width = self.scaleToZ( itemWidth, z, self.opt.perspective );
16626                    const diff = ( itemWidth - width ) / 2;
16627
16628                    offset -= diff;
16629
16630                    const gutterStep = 1;
16631                    const gutterValue = self.opt.gutter.unit === "%" ? itemWidth * Math.abs( gutter / 100 ) : Math.abs( gutter );
16632                    const gutterOffset = self.opt.gutter.unit === "%" ? self.scaleToZ( gutterValue, z, self.opt.perspective ) : gutterValue;
16633                    if (gutter > 0){
16634                        offset += gutterOffset;
16635                    } else {
16636                        offset -= gutterOffset;
16637                    }
16638                    if ( offset + width + diff > maxOffset ){
16639                        if ( gutter - gutterStep < self.opt.gutter.min ){
16640                            return self.calculate( itemWidth, maxOffset, self.opt.gutter.max, showPerSide - 1);
16641                        }
16642                        return self.calculate( itemWidth, maxOffset, gutter - gutterStep, showPerSide);
16643                    }
16644
16645                    const x = self.getVectorX( offset, z, self.opt.perspective );
16646
16647                    offset += width + diff;
16648
16649                    result.side.push({x: x, z: z, zIndex: zIndex });
16650                }
16651            }
16652            return result;
16653        },
16654        cleanup: function( selector, className, exclude ){
16655            const self = this;
16656            const hasExclude = _is.string( exclude );
16657            Array.from( self.el.querySelectorAll( selector ) ).forEach( function( node ){
16658
16659                node.classList.remove( className );
16660                if ( self.opt.centerOnClick ){
16661                    self._centerListeners.remove( node, "click" );
16662                }
16663
16664                if ( hasExclude && node.matches( exclude ) ) return;
16665
16666                node.style.removeProperty( "transition-duration" );
16667                node.style.removeProperty( "transform" );
16668                node.style.removeProperty( "z-index" );
16669            } );
16670        },
16671        renderActive: function( layout ){
16672            const self = this;
16673            if ( ! ( self.activeItem instanceof _.Item ) ) return false;
16674            const el = self.activeItem.el;
16675
16676            self.cleanup( self.sel.activeItem, self.cls.activeItem );
16677
16678            el.classList.add( self.cls.activeItem );
16679            el.style.setProperty("transition-duration", ( self._firstLayout ? 0 : self.opt.speed ) + "ms" );
16680            el.style.setProperty( "z-index", layout.zIndex );
16681            if ( _is.number( layout.activeX ) && layout.activeX !== 0 ){
16682                el.style.setProperty( "transform", "translate3d(" + layout.activeX + "px, 0, 0)" );
16683            } else {
16684                el.style.removeProperty( "transform" );
16685            }
16686
16687            const ai = self.tmpl.items && _is.fn( self.tmpl.items.indexOf ) ? self.tmpl.items.indexOf( self.activeItem ) : -1;
16688            Array.from( self.el.querySelectorAll( self.sel.bullet ) ).forEach( function( node, i ){
16689                node.classList.remove( self.cls.activeBullet );
16690                node.title = self.getBulletTitle( self.i18n.bullet, i + 1 );
16691                if ( i === ai ){
16692                    node.classList.add( self.cls.activeBullet );
16693                    node.title = self.getBulletTitle( self.i18n.activeBullet, i + 1 );
16694                }
16695            } );
16696            return true;
16697        },
16698        renderSide: function( side, selector, exclude, cls, layout ) {
16699            const self = this;
16700            if ( [ "left","right" ].indexOf( side ) === -1 ) return;
16701
16702            self.cleanup( selector, cls, exclude );
16703
16704            const valuesList = layout?.sideBySide?.[side] || layout.side;
16705            const activeX = _is.number( layout?.activeX ) ? layout.activeX : 0;
16706            let place = self.activeItem;
16707            for (let i = 0; i < valuesList.length; i++ ){
16708                const values = valuesList[i];
16709                const item = side === "left" ? self.getPrev( place ) : self.getNext( place );
16710                if ( item instanceof _.Item ){
16711                    const signedX = ( side === "left" && !self.isRTL ) || ( side === "right" && self.isRTL ) ? -values.x : values.x;
16712                    let vectorX = signedX;
16713
16714                    // Apply active position offset in screen space so perspective math remains bounded.
16715                    if ( activeX !== 0 ){
16716                        const screenX = self.getScreenX( signedX, values.z, self.opt.perspective ) + activeX;
16717                        vectorX = self.getVectorX( screenX, values.z, self.opt.perspective );
16718                    }
16719
16720                    let transform = "translate3d(" + vectorX + "px, 0,-" + values.z + "px)";
16721                    item.el.classList.add( cls );
16722                    if ( !item.isLoaded ){
16723                        item.el.style.setProperty("transition-duration", "0ms" );
16724                        item.el.style.setProperty( "transform", "translate3d(0,0,-" + self.opt.perspective + "px)" );
16725                        item.el.offsetHeight;
16726                    }
16727                    item.el.style.setProperty("transition-duration", ( self._firstLayout ? 0 : self.opt.speed ) + "ms" );
16728                    item.el.style.setProperty( "transform", transform );
16729                    item.el.style.setProperty( "z-index", values.zIndex );
16730
16731                    if ( self.opt.centerOnClick ){
16732                        self._centerListeners.add( item.el, "click", function( event ){
16733                            event.preventDefault();
16734                            event.stopPropagation();
16735                            self.interacted = true;
16736                            self.goto( item );
16737                        }, true );
16738                    }
16739
16740                    place = item;
16741                } else {
16742                    // exit early if no item was found
16743                    break;
16744                }
16745            }
16746        }
16747    });
16748
16749})(
16750    FooGallery.$,
16751    FooGallery,
16752    FooGallery.icons,
16753    FooGallery.utils,
16754    FooGallery.utils.is
16755);
16756
16757(function($, _, _obj){
16758
16759    _.CarouselTemplate = _.Template.extend({
16760        construct: function(options, element){
16761            const self = this;
16762            self._super(_obj.extend({}, options, {
16763                paging: {
16764                    type: "none"
16765                }
16766            }), element);
16767            self.items.LAYOUT_AFTER_LOAD = false;
16768            self.carousel = null;
16769            self.on({
16770                "pre-init": self.onPreInit,
16771                "init": self.onInit,
16772                "post-init": self.onPostInit,
16773                "destroyed": self.onDestroyed,
16774                "append-item": self.onAppendItem,
16775                "after-filter-change": self.onAfterFilterChange,
16776                "layout": self.onLayout
16777            }, self);
16778            if ( !!_.Panel && self.lightbox instanceof _.Panel ){
16779                self.lightbox.on({
16780                    "open": self.onLightboxOpen,
16781                    "closed": self.onLightboxClosed,
16782                    "next": self.onLightboxNext,
16783                    "prev": self.onLightboxPrev
16784                }, self);
16785            }
16786        },
16787        onPreInit: function(){
16788            const self = this;
16789            self.carousel = new _.Carousel( self, self.template, self.cls.carousel, self.sel.carousel, self.il8n );
16790        },
16791        onInit: function(){
16792            this.carousel.init();
16793        },
16794        onPostInit: function(){
16795            this.carousel.postInit();
16796        },
16797        onDestroyed: function(){
16798            const self = this;
16799            if (self.carousel instanceof _.Carousel){
16800                self.carousel.destroy();
16801            }
16802        },
16803        onAppendItem: function (event, item) {
16804            event.preventDefault();
16805            this.carousel.elem.inner.appendChild(item.el);
16806            item.isAttached = true;
16807        },
16808        onAfterFilterChange: function(){
16809            this.carousel.cache.delete( "layout" );
16810            this.carousel.interacted = true;
16811            this.carousel.goto(this.carousel.getFirst());
16812        },
16813        onLayout: function(){
16814            this.carousel.layout(this.lastWidth);
16815        },
16816        onLightboxOpen: function(){
16817            this.carousel.interacted = true;
16818            this.carousel.stop();
16819        },
16820        onLightboxClosed: function(){
16821            this.carousel.start();
16822        },
16823        onLightboxNext: function(){
16824            this.carousel.next();
16825        },
16826        onLightboxPrev: function(){
16827            this.carousel.previous();
16828        }
16829    });
16830
16831    _.template.register("carousel", _.CarouselTemplate, {
16832        template: {
16833            maxItems: 0, // "auto" or 0 will be calculated on the fly.
16834            perspective: 150,
16835            scale: 0.12,
16836            speed: 300,
16837            align: "center", // "center", "left" or "right". Visual alignment of the entire visible item set.
16838            activePosition: "center", // "center", "start" or "end". Controls active item position in the visible sequence.
16839            centerOnClick: true,
16840            gutter: {
16841                min: -40,
16842                max: -20,
16843                unit: "%"
16844            },
16845            autoplay: {
16846                time: 0,
16847                interaction: "pause" // "pause" or "disable"
16848            }
16849        }
16850    }, {
16851        container: "foogallery fg-carousel",
16852        carousel: {
16853            inner: "fg-carousel-inner",
16854            center: "fg-carousel-center",
16855            bottom: "fg-carousel-bottom",
16856            prev: "fg-carousel-prev",
16857            next: "fg-carousel-next",
16858            bullet: "fg-carousel-bullet",
16859            activeBullet: "fg-bullet-active",
16860            activeItem: "fg-item-active",
16861            prevItem: "fg-item-prev",
16862            nextItem: "fg-item-next",
16863            progress: "fg-carousel-progress"
16864        }
16865    }, {
16866        prev: "Previous",
16867        next: "Next",
16868        bullet: "Item {ITEM}",
16869        activeBullet: "Item {ITEM} - Current"
16870    });
16871
16872})(
16873    FooGallery.$,
16874    FooGallery,
16875    FooGallery.utils.obj
16876);
16877
16878(function($, _, _is, _fn, _obj, _t){
16879
16880	_.FooGridTemplate = _.Template.extend({
16881		construct: function(options, element){
16882			var self = this;
16883			self._super(options, element);
16884			if ( !self.template.noPanel ) {
16885				self.$section = null;
16886				self.isFirst = false;
16887				self.disableTransitions = false;
16888				self._panelLayoutTimeout = null;
16889				self._mql = [];
16890				self._didOpenByDefault = false;
16891				self.panel = new _.Panel( self, self.template );
16892				self.on({
16893					"pre-init": self.onPreInit,
16894					"parsed-item": self.onParsedItem,
16895					"created-item": self.onCreatedItem,
16896					"destroy-item": self.onDestroyItem,
16897					"after-state": self.onAfterState,
16898					"ready": self.onReady,
16899					"before-page-change": self.onBeforePageChange,
16900					"before-filter-change": self.onBeforeFilterChange
16901				}, self);
16902				self.panel.on({
16903					"next": self.onPanelNext,
16904					"prev": self.onPanelPrev,
16905					"close": self.onPanelClose,
16906					"area-load": self.onPanelAreaLoad,
16907					"area-unload": self.onPanelAreaUnload
16908				}, self);
16909			}
16910		},
16911		destroy: function(preserveState){
16912			var self = this, _super = self._super.bind(self);
16913			if ( self.panel ) {
16914				self.unbindMediaQueries();
16915				if (self._panelLayoutTimeout != null){
16916					clearTimeout(self._panelLayoutTimeout);
16917					self._panelLayoutTimeout = null;
16918				}
16919				return self.panel.destroy().then(function(){
16920					self.$section.remove();
16921					return _super(preserveState);
16922				});
16923			} else {
16924				return _super(preserveState);
16925			}
16926		},
16927
16928		onPreInit: function(){
16929			var self = this, hasTransition = false;
16930			if (/^foogrid-cols-[2-8]$/.test(self.template.columns || "")){
16931				self.$el.removeClass(function(index, className){
16932					return (className.match(/\bfoogrid-cols-\d+\b/g) || []).join(" ");
16933				}).addClass(self.template.columns);
16934			}
16935			self.$section = $('<section/>', {'class': 'foogrid-content'});
16936			self.bindMediaQueries();
16937			if (self.panel.opt.transition === "none"){
16938				if (self.$el.hasClass("foogrid-transition-horizontal")){
16939					self.panel.opt.transition = "horizontal";
16940					hasTransition = true;
16941				}
16942				if (self.$el.hasClass("foogrid-transition-vertical")){
16943					self.panel.opt.transition = "vertical";
16944					hasTransition = true;
16945				}
16946				if (self.$el.hasClass("foogrid-transition-fade")){
16947					self.panel.opt.transition = "fade";
16948					hasTransition = true;
16949				}
16950			}
16951			if (self.template.transitionOpen || self.template.transitionRow){
16952				hasTransition = hasTransition || self.$el.hasClass("foogrid-transition-horizontal foogrid-transition-vertical foogrid-transition-fade");
16953				self.template.transitionOpen = self.template.transitionOpen && hasTransition;
16954				self.template.transitionRow = self.template.transitionRow && hasTransition;
16955			}
16956			if (self.panel.opt.info === "none"){
16957				if (self.$el.hasClass("foogrid-caption-below")){
16958					self.panel.opt.info = "bottom";
16959				}
16960				if (self.$el.hasClass("foogrid-caption-right")){
16961					self.panel.opt.info = "right";
16962				}
16963			}
16964			if (self.panel.info.isEnabled() && self.panel.info.opt.align === "default"){
16965				var align = null;
16966				if (self.$el.hasClass("fg-c-l")) align = "left";
16967				if (self.$el.hasClass("fg-c-c")) align = "center";
16968				if (self.$el.hasClass("fg-c-r")) align = "right";
16969				if (self.$el.hasClass("fg-c-j")) align = "justified";
16970				if (align !== null) self.panel.info.opt.align = align;
16971			}
16972			if (self.panel.opt.theme === null){
16973				self.panel.opt.theme = self.getCSSClass("theme");
16974			}
16975			if (self.panel.opt.theme === "fg-light" && self.panel.opt.button === null){
16976				self.panel.opt.button = "fg-button-blue";
16977			}
16978			if (self.panel.opt.theme === "fg-dark" && self.panel.opt.button === null){
16979				self.panel.opt.button = "fg-button-dark";
16980			}
16981		},
16982		onParsedItem: function(event, item){
16983			if (item.isError) return;
16984			item.$anchor.off("click.foogallery").on("click.gg", {self: this, item: item}, this.onAnchorClick);
16985		},
16986		onCreatedItem: function(event, item){
16987			if (item.isError) return;
16988			item.$anchor.off("click.foogallery").on("click.gg", {self: this, item: item}, this.onAnchorClick);
16989		},
16990		onDestroyItem: function(event, item){
16991			if (item.isError) return;
16992			item.$anchor.off("click.gg", this.onAnchorClick);
16993		},
16994		onAfterState: function(event, state){
16995			if (!(state.item instanceof _.Item)) return;
16996			this.open(state.item);
16997		},
16998		onReady: function(){
16999			var self = this;
17000			if (self._didOpenByDefault) return;
17001			if (self.template.openByDefault !== true) return;
17002			if (self.panel.currentItem instanceof _.Item) return;
17003
17004			var $first = self.getVisibleItems().first(),
17005				firstItem = $first.length ? self.items.get($first.get(0), true) : self.items.first();
17006
17007			if (firstItem instanceof _.Item){
17008				self._didOpenByDefault = true;
17009				self.open(firstItem);
17010			}
17011		},
17012		onBeforePageChange: function(event, current, next, setPage, isFilter){
17013			if (isFilter) return;
17014			var self = this;
17015			if (!self.panel.isMaximized) self.close(true, self.panel.isAttached);
17016		},
17017		onBeforeFilterChange: function(){
17018			var self = this;
17019			if (!self.panel.isMaximized) self.close(true, self.panel.isAttached);
17020		},
17021		onMediaQueryChange: function(){
17022			var self = this;
17023			if (self._panelLayoutTimeout != null){
17024				clearTimeout(self._panelLayoutTimeout);
17025			}
17026			self._panelLayoutTimeout = setTimeout(function(){
17027				self._panelLayoutTimeout = null;
17028				self.repositionPanelSection();
17029			}, 60);
17030		},
17031		bindMediaQueries: function(){
17032			var self = this;
17033			var queries = [
17034				"(min-width: 1441px) and (max-width: 1600px)",
17035				"(min-width: 1201px) and (max-width: 1440px)",
17036				"(min-width: 993px) and (max-width: 1200px)",
17037				"(min-width: 769px) and (max-width: 992px)",
17038				"(min-width: 481px) and (max-width: 768px)",
17039				"(max-width: 480px)"
17040			];
17041			self.unbindMediaQueries();
17042			self._mql = queries.map(function(query){
17043				var mql = window.matchMedia(query);
17044				var handler = self.onMediaQueryChange.bind(self);
17045				if (mql.addEventListener){
17046					mql.addEventListener("change", handler);
17047				} else if (mql.addListener){
17048					mql.addListener(handler);
17049				}
17050				return {
17051					mql: mql,
17052					handler: handler
17053				};
17054			});
17055		},
17056		unbindMediaQueries: function(){
17057			var self = this;
17058			if (!self._mql || !self._mql.length) return;
17059			self._mql.forEach(function(entry){
17060				var mql = entry.mql, handler = entry.handler;
17061				if (mql.removeEventListener){
17062					mql.removeEventListener("change", handler);
17063				} else if (mql.removeListener){
17064					mql.removeListener(handler);
17065				}
17066			});
17067			self._mql = [];
17068		},
17069
17070		onPanelNext: function(event, currentItem, nextItem){
17071			event.preventDefault();
17072			this.open(nextItem);
17073		},
17074		onPanelPrev: function(event, currentItem, prevItem){
17075			event.preventDefault();
17076			this.open(prevItem);
17077		},
17078		onPanelClose: function(event){
17079			event.preventDefault();
17080			this.close(false, true);
17081		},
17082		onPanelAreaLoad: function(event, area, media){
17083			if (area.name === "content"){
17084				media.item.$el.addClass(this.cls.visible);
17085			}
17086		},
17087		onPanelAreaUnload: function(event, area, media){
17088			if (area.name === "content"){
17089				media.item.$el.removeClass(this.cls.visible);
17090			}
17091		},
17092
17093		onAnchorClick: function(e){
17094			e.preventDefault();
17095			e.data.self.toggle(e.data.item);
17096		},
17097
17098		transitionsEnabled: function(){
17099			return !this.disableTransitions && this.panel.hasTransition;
17100		},
17101		isNewRow: function( item ){
17102			var self = this,
17103				oldTop = self.getOffsetTop(self.panel.currentItem),
17104				newTop = self.getOffsetTop(item);
17105			return oldTop !== newTop;
17106		},
17107		getOffsetTop: function(item){
17108			return item instanceof _.Item && item.isCreated ? item.$el.offset().top : 0;
17109		},
17110		getBaseColumns: function(){
17111			var self = this,
17112				match = (self.$el.attr("class") || "").match(/\bfoogrid-cols-(\d+)\b/);
17113			return match ? parseInt(match[1], 10) : 0;
17114		},
17115		getForcedMaxColumns: function(){
17116			var width = window.innerWidth || document.documentElement.clientWidth || 0;
17117			if (width <= 480) return 2;
17118			if (width <= 768) return 3;
17119			if (width <= 992) return 4;
17120			if (width <= 1200) return 5;
17121			if (width <= 1440) return 6;
17122			if (width <= 1600) return 7;
17123			return 0;
17124		},
17125		getEffectiveColumns: function(){
17126			var base = this.getBaseColumns(),
17127				forced = this.getForcedMaxColumns();
17128			if (!base) return 0;
17129			return forced > 0 ? Math.min(base, forced) : base;
17130		},
17131		getVisibleItems: function(){
17132			return this.$el.children(".fg-item").filter(function(){
17133				return $(this).css("display") !== "none";
17134			});
17135		},
17136		getRowLastItem: function(item){
17137			if (!(item instanceof _.Item) || !item.isCreated){
17138				return item instanceof _.Item ? item.$el : null;
17139			}
17140			var self = this,
17141				columns = self.getEffectiveColumns(),
17142				$items = self.getVisibleItems(),
17143				currentIndex = $items.index(item.$el);
17144
17145			if (!columns || currentIndex === -1){
17146				return item.$el;
17147			}
17148
17149			var rowEndIndex = Math.min(
17150				(Math.floor(currentIndex / columns) * columns) + (columns - 1),
17151				$items.length - 1
17152			);
17153			return $items.eq(rowEndIndex);
17154		},
17155		repositionPanelSection: function(){
17156			var self = this;
17157			if (!(self.panel.currentItem instanceof _.Item)) return;
17158			if (self.panel.isMaximized) return;
17159			if (!self.$section || self.$section.parent().length === 0) return;
17160
17161			var $rowLast = self.getRowLastItem(self.panel.currentItem);
17162			if (!$rowLast || !$rowLast.length) return;
17163
17164			if ($rowLast.next().get(0) !== self.$section.get(0)){
17165				$rowLast.after(self.$section);
17166			}
17167		},
17168		open: function(item){
17169			var self = this;
17170			if (item.index !== -1){
17171				var newRow = self.isNewRow(item);
17172				if (self.panel.currentItem instanceof _.Item && newRow && !self.panel.isMaximized){
17173					return self.doClose(newRow).then(function(){
17174						if (!!self.pages && !self.pages.contains(self.pages.current, item)){
17175							self.pages.goto(self.pages.find(item));
17176						}
17177						return self.doOpen(item, newRow);
17178					});
17179				}
17180				if (!!self.pages && !self.pages.contains(self.pages.current, item)){
17181					self.pages.goto(self.pages.find(item));
17182				}
17183				return self.doOpen(item, newRow);
17184			}
17185			return $.when();
17186		},
17187		doOpen: function(item, newRow){
17188			var self = this;
17189			return $.Deferred(function(def){
17190				self.panel.appendTo(self.$section);
17191				if (newRow){
17192					var $rowLast = self.getRowLastItem(item);
17193					if ($rowLast && $rowLast.length){
17194						$rowLast.after(self.$section);
17195					} else {
17196						item.$el.after(self.$section);
17197					}
17198				}
17199				if (self.transitionOpen(newRow)){
17200					self.isFirst = false;
17201					_t.start(self.$section, function($el){
17202						$el.addClass(self.cls.visible);
17203					}, null, 350).then(function(){
17204						def.resolve();
17205					}, function(err){
17206						def.reject(err);
17207					});
17208				} else {
17209					self.$section.addClass(self.cls.visible);
17210					def.resolve();
17211				}
17212			}).then(function(){
17213				const behavior = self.template.scrollSmooth && (newRow || self.isFirst) ? "smooth" : "instant";
17214				self.$section.get(0).scrollIntoView({
17215					behavior: behavior,
17216					block: "center",
17217					inline: "center"
17218				});
17219			}).then(function(){
17220				return self.panel.load(item);
17221			}).then(function(){
17222				self.$section.trigger('focus');
17223			}).always(function(){
17224				self.isBusy = false;
17225			}).promise();
17226		},
17227		transitionOpen: function(newRow){
17228			return this.transitionsEnabled() && !this.panel.isMaximized && ((this.template.transitionOpen && this.isFirst) || (this.template.transitionRow && newRow));
17229		},
17230		close: function(immediate, newRow){
17231			immediate = _is.boolean(immediate) ? immediate : false;
17232			var self = this, previous = self.disableTransitions;
17233			self.disableTransitions = immediate;
17234			return self.doClose(newRow).then(function(){
17235				self.disableTransitions = previous;
17236			});
17237		},
17238		doClose: function(newRow){
17239			var self = this;
17240			return $.Deferred(function(def){
17241				if (self.panel.currentItem instanceof _.Item){
17242					self.panel.currentItem.$el.removeClass(self.cls.visible);
17243					if (self.transitionClose(newRow)){
17244						_t.start(self.$section, function($el){
17245							$el.removeClass(self.cls.visible);
17246						}, null, 350).then(function(){
17247							self.panel.doClose(true, true).then(function(){
17248								def.resolve();
17249							}, function(err){
17250								def.reject(err);
17251							});
17252						}, function(err){
17253							def.reject(err);
17254						});
17255					} else {
17256						self.$section.removeClass(self.cls.visible);
17257						self.panel.doClose(true, true).then(function(){
17258							def.resolve();
17259						}, function(err){
17260							def.reject(err);
17261						});
17262					}
17263				} else {
17264					def.resolve();
17265				}
17266			}).always(function(){
17267				self.$section.detach();
17268				self.isFirst = true;
17269			}).promise();
17270		},
17271		transitionClose: function(newRow){
17272			return this.transitionsEnabled() && !this.panel.isMaximized && ((this.template.transitionRow && newRow) || (this.template.transitionOpen && !newRow));
17273		},
17274		toggle: function(item){
17275			var self = this;
17276			if (item instanceof _.Item){
17277				if (self.panel.currentItem === item){
17278					return self.close();
17279				} else {
17280					return self.open(item);
17281				}
17282			}
17283			return _fn.reject();
17284		}
17285	});
17286
17287	_.template.register("foogrid", _.FooGridTemplate, {
17288		template: {
17289			noPanel: false,
17290			classNames: "foogrid-panel",
17291			scroll: true,
17292			scrollSmooth: false,
17293			loop: true,
17294			external: '_blank',
17295			externalText: null,
17296			keyboard: true,
17297			transitionRow: true,
17298			transitionOpen: true,
17299			openByDefault: false,
17300			noMobile: true,
17301			info: "bottom",
17302            infoVisible: true,
17303            infoOverlay: false,
17304			infoAutoHide: false,
17305			buttons: {
17306				fullscreen: false,
17307			}
17308		}
17309	}, {
17310		container: "foogallery foogrid",
17311		visible: "foogrid-visible"
17312	});
17313
17314})(
17315	FooGallery.$,
17316	FooGallery,
17317	FooGallery.utils.is,
17318	FooGallery.utils.fn,
17319	FooGallery.utils.obj,
17320	FooGallery.utils.transition
17321);
17322
17323(function($, _, _utils, _obj){
17324
17325    _.SliderTemplate = _.Template.extend({
17326        construct: function(options, element){
17327            var self = this;
17328            self._super(_obj.extend({}, options, {
17329                paging: {
17330                    type: "none"
17331                }
17332            }), element);
17333            self.items.ALLOW_CREATE = false;
17334            self.items.ALLOW_APPEND = false;
17335            self.panel = new _.Panel(self, self.template);
17336        },
17337        preInit: function(){
17338            var self = this;
17339            if (self._super()){
17340                self.$el.toggleClass(self.cls.fitContainer, self.template.fitContainer);
17341                self.template.horizontal = self.$el.hasClass("fgs-horizontal") || self.template.horizontal;
17342                if (self.panel.opt.thumbs === null){
17343                    self.panel.thumbs.opt.position = self.template.horizontal ? "bottom" : "right";
17344                }
17345                if (self.$el.hasClass("fgs-no-captions")){
17346                    self.template.noCaptions = true;
17347                    self.panel.thumbs.opt.captions = !self.template.noCaptions;
17348                }
17349                if (self.$el.hasClass("fgs-content-nav")){
17350                    self.template.contentNav = true;
17351                    self.panel.opt.buttons.prev = self.panel.opt.buttons.next = self.template.contentNav;
17352                }
17353                if (self.panel.opt.button === null){
17354                    self.panel.opt.button = this.getPanelButtonClass();
17355                }
17356                if (self.panel.info.isEnabled() && self.panel.info.opt.align === "default"){
17357                    var align = null;
17358                    if (self.$el.hasClass("fg-c-l")) align = "left";
17359                    if (self.$el.hasClass("fg-c-c")) align = "center";
17360                    if (self.$el.hasClass("fg-c-r")) align = "right";
17361                    if (self.$el.hasClass("fg-c-j")) align = "justified";
17362                    if (align !== null) self.panel.info.opt.align = align;
17363                }
17364                return true;
17365            }
17366            return false;
17367        },
17368        ready: function(){
17369            var self = this;
17370            if (self._super()){
17371                self.panel.appendTo(self.$el);
17372                self.panel.load(self.state.current.item);
17373                return true;
17374            }
17375            return false;
17376        },
17377        destroy: function(preserveState){
17378            var self = this, _super = self._super.bind(self);
17379            return self.panel.destroy().then(function(){
17380                return _super(preserveState);
17381            });
17382        },
17383        getPanelButtonClass: function(){
17384            var className = this.$el.prop("className"),
17385                match = /(?:^|\s)fgs-(purple|red|green|blue|orange)(?:$|\s)/.exec(className);
17386
17387            return match != null && match.length >= 2 ? "fg-button-" + match[1] : null;
17388        },
17389    });
17390
17391    _.template.register("slider", _.SliderTemplate, {
17392        template: {
17393            horizontal: false,
17394            noCaptions: false,
17395            contentNav: false,
17396
17397            fitContainer: false,
17398            fitMedia: true,
17399            transition: "horizontal",
17400            hoverButtons: true,
17401            preserveButtonSpace: false,
17402            noMobile: true,
17403            thumbs: null,
17404            thumbsSmall: true,
17405            info: "top",
17406            infoVisible: true,
17407            infoAutoHide: false,
17408            buttons: {
17409                close: false,
17410                info: false,
17411                maximize: false,
17412                fullscreen: false
17413            },
17414            // outerBreakpoints: {
17415            //     "x-small": 480,
17416            //     small: 768,
17417            //     medium: 1024,
17418            //     large: 1280,
17419            //     "x-large": 1600
17420            // }
17421        }
17422    }, {
17423        container: "foogallery fg-slider",
17424        fitContainer: "fg-fit-container"
17425    });
17426
17427})(
17428    FooGallery.$,
17429    FooGallery,
17430    FooGallery.utils,
17431    FooGallery.utils.obj
17432);
17433(function($, _, _utils, _is, _obj, _t){
17434
17435    _.StackAlbum = _utils.Class.extend({
17436        construct: function(element, options){
17437            var self = this;
17438            self.$el = _is.jq(element) ? element : $(element);
17439            self.el = self.$el.get(0);
17440            self.opt = _obj.extend({}, _.StackAlbum.defaults, options, self.$el.data('foogallery'));
17441            self.$back = self.$el.find('.fg-header-back');
17442            self.$active = self.$el.find('.fg-header-active');
17443            self.$piles = self.$el.find('.fg-piles');
17444            self.piles = self.$piles.find('.fg-pile').map(function(i, el){
17445                return new _.StackAlbum.Pile(self, el, { index: i });
17446            }).get();
17447            self.ignoreResize = false;
17448            self.info = self.getLayoutInfo();
17449            self.robserver = new ResizeObserver(function (e) {
17450                if (!self.ignoreResize && self.$el.is(":visible")){
17451                    const width = self.$el.width();
17452                    if ( self.info.maxWidth !== width ) {
17453                        self.info = self.getLayoutInfo();
17454                        self.layout(true);
17455                    }
17456                }
17457            });
17458        },
17459        init: function(){
17460            var self = this;
17461            self.info = self.getLayoutInfo();
17462            self.piles.forEach(function(pile){
17463                pile.init();
17464            });
17465            self.$back.on('click.foogallery', {self: self}, self.onBackClick);
17466            self.layout(true);
17467            self.robserver.observe(self.el);
17468        },
17469        destroy: function(){
17470            var self = this;
17471            self.robserver.disconnect();
17472            self.$back.off('.foogallery');
17473            self.piles.forEach(function(pile){
17474                pile.destroy();
17475            });
17476        },
17477        // getLayoutInfo: function(){
17478        //     var self = this,
17479        //         space = self.opt.gutter + (self.opt.border*2);
17480        //     return {
17481        //         maxWidth: self.$el.width(),
17482        //         space: space,
17483        //         halfSpace: space/2,
17484        //         itemWidth: self.opt.itemWidth,
17485        //         itemHeight: self.opt.itemHeight,
17486        //         itemOuterWidth: self.opt.itemWidth + (self.opt.border*2),
17487        //         itemOuterHeight: self.opt.itemHeight + (self.opt.border*2),
17488        //         blockWidth: self.opt.itemWidth + space,
17489        //         blockHeight: self.opt.itemHeight + space,
17490        //         border: self.opt.border,
17491        //         doubleBorder: self.opt.border*2,
17492        //         gutter: self.opt.gutter,
17493        //         halfGutter: self.opt.gutter/2
17494        //     };
17495        // },
17496        getLayoutInfo: function(){
17497            const self = this,
17498                maxWidth = self.$el.width(),
17499                doubleBorder = self.opt.border * 2,
17500                space = self.opt.gutter + doubleBorder,
17501                maxWidthInner = maxWidth - space;
17502
17503            let ratio = 1;
17504            if ( self.opt.itemWidth > maxWidthInner ) {
17505                ratio = maxWidthInner / self.opt.itemWidth;
17506            }
17507
17508            const itemWidth = self.opt.itemWidth * ratio,
17509                itemHeight = self.opt.itemHeight * ratio;
17510
17511            return {
17512                maxWidth: maxWidth,
17513                space: space,
17514                halfSpace: space/2,
17515                itemWidth: itemWidth,
17516                itemHeight: itemHeight,
17517                itemOuterWidth: itemWidth + doubleBorder,
17518                itemOuterHeight: itemHeight + doubleBorder,
17519                blockWidth: itemWidth + space,
17520                blockHeight: itemHeight + space,
17521                border: self.opt.border,
17522                doubleBorder: doubleBorder,
17523                gutter: self.opt.gutter,
17524                halfGutter: self.opt.gutter/2
17525            };
17526        },
17527        layout: function(immediate){
17528            var self = this, size;
17529            if (immediate){
17530                self.$el.addClass('fg-disable-transitions');
17531                self.$el.prop('offsetWidth');
17532            }
17533            if (self.hasActive){
17534                size = self.activePile.layout();
17535                self.activePile.setPosition(0, 0, size.width, size.height);
17536                self.$piles.css({width: size.width + 'px', height: size.height + 'px'});
17537            } else {
17538                size = self.layoutPiles();
17539                self.$piles.css({width: size.width + 'px', height: size.height + 'px'});
17540            }
17541            if (immediate){
17542                setTimeout(function(){
17543                    self.$el.removeClass('fg-disable-transitions');
17544                }, 0);
17545            }
17546        },
17547        layoutPiles: function(callback){
17548            var self = this,
17549                rowWidth = 0, rowCount = 1, width = 0;
17550
17551            callback = _is.fn(callback) ? callback : function(){};
17552
17553            self.piles.forEach(function(pile){
17554                var left = rowWidth;
17555                rowWidth += self.info.blockWidth;
17556                if (rowWidth > self.info.maxWidth && left > 0){
17557                    left = 0;
17558                    rowWidth = self.info.blockWidth;
17559                    rowCount++;
17560                }
17561                var top = self.info.blockHeight * (rowCount - 1);
17562                callback(pile, self.info);
17563                pile.layoutCollapsed();
17564                pile.setPosition(top, left, self.info.blockWidth, self.info.blockHeight);
17565                // keep track of the max calculated width
17566                if (rowWidth > width) width = rowWidth;
17567            });
17568            return {
17569                width: width,
17570                height: self.info.blockHeight * rowCount
17571            };
17572        },
17573        setActive: function(pile){
17574            var self = this,
17575                previous = self.activePile,
17576                hadActive = previous instanceof _.StackAlbum.Pile,
17577                size;
17578
17579            pile = pile instanceof _.StackAlbum.Pile ? pile : null;
17580
17581            self.activePile = pile;
17582            self.hasActive = pile !== null;
17583
17584            if (hadActive){
17585                previous.collapse();
17586            }
17587
17588            self.ignoreResize = true;
17589            if (self.hasActive){
17590                self.piles.forEach(function(p){
17591                    if (p === pile) return;
17592                    p.hide(self.activePile);
17593                });
17594                size = self.activePile.expand();
17595                self.$active.text(pile.title);
17596                self.$el.addClass('fg-has-active');
17597            } else {
17598                size = self.layoutPiles(function(p){
17599                    p.show();
17600                });
17601                self.$el.removeClass('fg-has-active');
17602            }
17603            _t.start(self.$piles, function($el){
17604                $el.css({width: size.width + 'px', height: size.height + 'px'});
17605            }, null, 350).then(function(){
17606                self.ignoreResize = false;
17607            });
17608        },
17609        onBackClick: function(e){
17610            e.preventDefault();
17611            e.stopPropagation();
17612            e.data.self.setActive(null);
17613        }
17614    });
17615
17616    _.StackAlbum.defaults = {
17617        gutter: 50,
17618        itemWidth: 150,
17619        itemHeight: 150,
17620        border: 10,
17621        angleStep: 1,
17622        randomAngle: false
17623    };
17624
17625})(
17626    FooGallery.$,
17627    FooGallery,
17628    FooGallery.utils,
17629    FooGallery.utils.is,
17630    FooGallery.utils.obj,
17631    FooGallery.utils.transition
17632);
17633(function($, _, _utils, _is, _obj){
17634
17635    _.StackAlbum.Pile = _utils.Class.extend({
17636        construct: function(album, element, options){
17637            var self = this;
17638            self.album = album;
17639            self.$el = _is.jq(element) ? element : $(element);
17640            self.opt = _obj.extend({}, _.StackAlbum.Pile.defaults, options, self.$el.data());
17641            self.title = self.opt.title;
17642            self.description = self.opt.description;
17643            self.captionAlignment = ['left', 'center', 'right'].indexOf(self.opt.captionAlignment) !== -1 ? self.opt.captionAlignment : 'center';
17644            self.items = self.$el.find('.fg-pile-item').map(function(i, el){
17645                return new _.StackAlbum.Item(self, el, { index: i });
17646            }).get();
17647            self.$cover = self.$el.find('.fg-pile-cover').first();
17648            self.top = 0;
17649            self.left = 0;
17650            self.isExpanded = false;
17651        },
17652        init: function(){
17653            var self = this,
17654                opt = self.album.opt,
17655                availableAngles = self.getAngles(opt.angleStep),
17656                currentAngle = opt.randomAngle ? self.randomAngle(availableAngles) : opt.angleStep;
17657
17658            if ( self.$cover.length === 0 && self.items.length > 0 ) {
17659                const $coverContent = $('<div/>', {'class': 'fg-pile-cover-content fg-pile-caption-align-' + self.captionAlignment}).append(
17660                    $('<span/>', {'class': 'fg-pile-cover-title', text: self.opt.title}),
17661                    $('<span/>', {'class': 'fg-pile-cover-count', text: self.items.length})
17662                );
17663
17664                if ( self.description ) {
17665                    $coverContent.append(
17666                        $('<span/>', {'class': 'fg-pile-cover-description', html: self.description})
17667                    );
17668                }
17669
17670                self.$cover = $('<div/>', {'class': 'fg-pile-cover'}).append(
17671                    $coverContent
17672                );
17673                self.items[0].$el.addClass('fg-has-cover').append(self.$cover);
17674            }
17675            self.$cover.on('click.foogallery', {self: self}, self.onCoverClick);
17676            self.items.forEach(function(item, i){
17677                item.init();
17678                if (i > 3) return; // we only care about the first 4 items after init
17679                if (i === 0){
17680                    item.load();
17681                } else {
17682                    if (i % 2 === 0){
17683                        item.setAngle(-currentAngle);
17684                    } else {
17685                        item.setAngle(currentAngle);
17686                    }
17687                    if (opt.randomAngle){
17688                        currentAngle = self.randomAngle(availableAngles);
17689                    } else {
17690                        currentAngle += opt.angleStep;
17691                    }
17692                }
17693            });
17694        },
17695        destroy: function(){
17696            var self = this;
17697            self.$cover.remove();
17698            self.items.forEach(function(item, i){
17699                if (i === 0) item.$el.removeClass('fg-has-cover');
17700                item.destroy();
17701            });
17702        },
17703        getAngles: function(step){
17704            var result = [], i = 1;
17705            for (; i <= 3; i++){
17706                result.push(i * step);
17707            }
17708            return result;
17709        },
17710        randomAngle: function(available){
17711            var min = 0, max = available.length,
17712                index = Math.floor(Math.random() * (max - min) + min),
17713                angle = available.splice(index, 1);
17714            return angle.length === 1 ? angle[0] : 0;
17715        },
17716        setPosition: function(top, left, itemWidth, itemHeight){
17717            var self = this;
17718            self.top = top;
17719            self.left = left;
17720            if (_is.number(itemWidth) && _is.number(itemHeight)){
17721                self.$el.css({top: top + 'px', left: left + 'px', width: itemWidth + 'px', height: itemHeight + 'px'});
17722            } else {
17723                self.$el.css({top: top + 'px', left: left + 'px'});
17724            }
17725        },
17726        layout: function(){
17727            const self = this,
17728                info = self.album.info;
17729
17730            if ( !self.isExpanded ) {
17731                return self.layoutCollapsed();
17732            }
17733
17734            let rowWidth = 0, rowCount = 1,
17735                isNew = false, width = 0;
17736
17737            self.items.forEach(function(item){
17738                rowWidth += info.halfGutter;
17739                if (rowWidth > info.maxWidth){
17740                    rowWidth = info.halfGutter;
17741                    rowCount++;
17742                    isNew = true;
17743                    console.log("A");
17744                }
17745                var left = rowWidth;
17746                rowWidth += info.itemOuterWidth + info.halfGutter;
17747                if (!isNew && rowWidth > info.maxWidth){
17748                    left = info.halfGutter;
17749                    rowWidth = info.blockWidth;
17750                    rowCount++;
17751                    console.log("B");
17752                }
17753                var top = (info.blockHeight * (rowCount - 1)) + info.halfGutter;
17754                isNew = false;
17755                item.setPosition(top, left, info.itemOuterWidth, info.itemOuterHeight);
17756                if (!item.isLoaded) item.load();
17757                // keep track of the max calculated width
17758                if (rowWidth > width) width = rowWidth;
17759            });
17760            return {
17761                width: width,
17762                height: info.blockHeight * rowCount
17763            };
17764        },
17765
17766        layoutCollapsed: function(){
17767            const self = this,
17768                info = self.album.info;
17769            self.items.forEach(function(item){
17770                item.setPosition(info.halfGutter, info.halfGutter, info.itemOuterWidth, info.itemOuterHeight);
17771            });
17772            return {
17773                width: info.blockWidth,
17774                height: info.blockHeight
17775            };
17776        },
17777
17778        expand: function(){
17779            var self = this, size;
17780            self.$el.removeClass('fg-collapsed').addClass('fg-expanded');
17781            self.isExpanded = true;
17782            size = self.layout();
17783            self.setPosition(0, 0, size.width, size.height);
17784            return size;
17785        },
17786        collapse: function(){
17787            var self = this, size;
17788            self.$el.removeClass('fg-expanded').addClass('fg-collapsed');
17789            self.isExpanded = false;
17790            size = self.layout();
17791            self.setPosition(0, 0, size.width, size.height);
17792            return size;
17793        },
17794        show: function(){
17795            var self = this;
17796            self.$el.removeClass('fg-hidden fg-expanded fg-collapsed');
17797        },
17798        hide: function(behind){
17799            var self = this;
17800            if (behind instanceof _.StackAlbum.Pile){
17801                self.setPosition(behind.top, behind.left);
17802            }
17803            self.$el.addClass('fg-hidden');
17804        },
17805
17806        onCoverClick: function(e){
17807            if ($(e.target).closest('a, button, input, select, textarea, [role="button"]').length !== 0){
17808                return;
17809            }
17810            e.preventDefault();
17811            e.stopPropagation();
17812            var self = e.data.self;
17813            self.album.setActive(self);
17814        }
17815    });
17816
17817    _.StackAlbum.Pile.defaults = {
17818        index: -1,
17819        title: null,
17820        captionAlignment: 'center',
17821        description: ''
17822    };
17823
17824})(
17825    FooGallery.$,
17826    FooGallery,
17827    FooGallery.utils,
17828    FooGallery.utils.is,
17829    FooGallery.utils.obj
17830);
17831
17832(function($, _, _utils, _is, _obj){
17833
17834    _.StackAlbum.Item = _utils.Class.extend({
17835        construct: function(pile, element, options){
17836            var self = this;
17837            self.pile = pile;
17838            self.$el = _is.jq(element) ? element : $(element);
17839            self.opt = _obj.extend({}, _.StackAlbum.Item.defaults, options, self.$el.data());
17840            self.$thumb = self.$el.find('.fg-pile-item-thumb');
17841            self.$image = self.$el.find('.fg-pile-item-image');
17842            self.isLoaded = false;
17843            self.isLoading = false;
17844            self._loading = null;
17845        },
17846        init: function(){
17847            const self = this,
17848                info = self.pile.album.info;
17849            self.$el.css({width: info.itemOuterWidth + 'px', height: info.itemOuterHeight + 'px'});
17850        },
17851        destroy: function(){
17852            const self = this;
17853            self.$el.css({top: '', left: '', width: '', height: '', transform: ''});
17854        },
17855        setAngle: function(angle){
17856            var self = this;
17857            self.$el.css({transform: 'rotate(' + angle + 'deg)'});
17858        },
17859        setPosition: function(top, left, itemWidth, itemHeight){
17860            var self = this;
17861            self.$el.css({top: top + 'px', left: left + 'px', width: itemWidth + 'px', height: itemHeight + 'px'});
17862        },
17863        load: function(){
17864            var self = this;
17865            if (_is.promise(self._loading)) return self._loading;
17866            return self._loading = $.Deferred(function(def){
17867                self.$el.addClass('fg-loading');
17868                self.isLoading = true;
17869                self.$image.on({
17870                    'load.foogallery': function(){
17871                        self.$image.off('.foogallery');
17872                        self.$el.removeClass('fg-loading');
17873                        self.isLoading = false;
17874                        self.isLoaded = true;
17875                        def.resolve();
17876                    },
17877                    'error.foogallery': function(){
17878                        self.$image.off('.foogallery');
17879                        self.$el.removeClass('fg-loading');
17880                        self.isLoading = false;
17881                        self.isLoaded = true;
17882                        def.reject();
17883                    }
17884                });
17885                self.$image.prop('src', self.$image.attr(self.opt.src))
17886                    .prop('srcset', self.$image.attr(self.opt.srcset));
17887            }).promise();
17888        }
17889    });
17890
17891    _.StackAlbum.Item.defaults = {
17892        index: -1,
17893        src: 'data-src-fg',
17894        srcset: 'data-srcset-fg'
17895    };
17896
17897})(
17898    FooGallery.$,
17899    FooGallery,
17900    FooGallery.utils,
17901    FooGallery.utils.is,
17902    FooGallery.utils.obj
17903);
17904(function ($, _, _utils) {
17905
17906    $.fn.foogalleryStackAlbum = function(options){
17907        return this.each(function(i, el){
17908            var $el = $(el), inst = $el.data('__FooGalleryAlbum__');
17909            if (inst instanceof _.StackAlbum) inst.destroy();
17910            inst = new _.StackAlbum($el);
17911            inst.init();
17912            $el.data('__FooGalleryAlbum__', inst);
17913        });
17914    };
17915
17916    _.loadStackAlbums = _.reloadStackAlbums = function(){
17917        // this automatically initializes all templates on page load
17918        $(function () {
17919            $('.foogallery-stack-album:not(.fg-ready)').foogalleryStackAlbum();
17920        });
17921
17922        _utils.ready(function () {
17923            $('.foogallery-stack-album.fg-ready').foogalleryStackAlbum();
17924        });
17925    };
17926
17927    _.loadStackAlbums();
17928
17929})(
17930    FooGallery.$,
17931    FooGallery,
17932    FooGallery.utils
17933);

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.