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|<length> - 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="…"] - 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 : "…"); 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, '&' ).replace( /</g, '<' ).replace( />/g, '>' ); 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: "«", 10998 prev: "‹", 10999 next: "›", 11000 last: "»", 11001 prevMore: "…", 11002 nextMore: "…" 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.