PageSourceSearch

https://apidarium.de/res/all.js?v=8.1.1

js apidarium.de collected 2026-10-08 17:49:22 UTC 611,573 bytes, 20,463 lines download raw bytes

1/* all.js - Tiger skin scripts */
2var VER='8.1.1',DEBUG=true,LOCALE='en-US';
3;Texts = {and:'and',from:'From {0}',tooLong:'This is too long to send directly to the email application.',to:'To',subject:'Subject',copyInstructions:'The text has been copied to your clipboard. Now switch to the email application (or webmail) and paste it. Move "To" and "Subject" into the appropriate boxes!',okButton:'OK',warning:'Warning',copiedToClipboard:'Copied to clipboard!',continueBrowsing:'Continue browsing',removeAll:'Remove all',feedbackButtonExplanation:'Try "Send" if you have an email application installed, use "Copy" for webmail!',removeAllItems:'Remove all items?',yes:'Yes',no:'No',rating:'Rating',print:'Print',printLabel:'Print out this photo on your printer',download:'Download',addCart:'Add to Cart',shopLabel:'Add this item to the shopping cart',viewCart:'View Cart',viewCartLabel:'View shopping cart',fotomotoBtn:'Buy / Share',fotomotoTooltip:'Buy prints or digital files, share, send free eCards',metaBtn:'Photo data',metaLabel:'Display photographic (Exif/Iptc) data',regionsBtn:'People',regionsLabel:'Show tagged people',mapBtn:'Map',mapLabel:'Show the photo location on the map',mostphotosBtn:'Purchase',mostphotosLabel:'Download this image from <b>mostphotos.com</b>!',share:'Share',shareLabel:'Share this photo over social sites',shareOn:'Share on',play:'Play',audioClipLabel:'Play / pause the attached audio clip',startOver:'Start over',upOneLevel:'Up one level',backToHome:'Back to home',backToIndex:'Back to index page',nextFolder:'Next folder',nextIndex:'Next index page',atLastPageQuestion:'Where to go next?',atLastPage:'At last page',clickToOpen:'Click to open this document with the associated viewer',pauseShort:'Pause',startSlideshowShort:'Play',previousPicture:'Previous picture',previousPictureShort:'Prev',upOneLevelShort:'Exit',zoom:'Zoom',oneToOneSize:'1:1 size',oneToOneSizeShort:'1:1',fitToScreen:'Fit to screen',fitToScreenShort:'Fit',showThumbs:'Show thumbnails',showThumbsShort:'Thumbnails',hideThumbs:'Hide thumbnails',hideThumbsShort:'Hide thumbs',showInfo:'Show caption / info',showInfoShort:'Info',hideInfo:'Hide caption / info',hideInfoShort:'Hide info',slideshow:'Slideshow',timing:'Timing',startStopSlideshow:'Start / stop slideshow',fullscreen:'Fullscreen',fullscreenShort:'Fullscr',exitFullscreen:'Exit fullscreen',exitFullscreenShort:'Normal',nextPicture:'Next picture',nextPictureShort:'Next',closeWindow:'Close window',noMoreItems:'You can´t buy more than 99 items at once on PayPal. Added only the first 99.',maxNItems:'Maximum <b>{0}</b> items allowed!',items:'items',item:'item',couponRemoved:'The coupon has been removed.',addMoreItems:'Add {0} more item(s) to use this coupon!',validAbove:'Valid only above {0} cart value.',amountLowerThan:'The amount is lower than the discount <b>{0}</b>.',cartIsEmpty:'The cart is empty',minimumCartValue:'The minimal cart value is <b>{0}</b>',proceedToPayPal:'Proceed to PayPal!',expired:'The coupon code <b>{0}</b> has expired!',minAmountWarning:'This coupon can only be used if the total amount exceeds <b>{0}</b>.',minQuantityWarning:'This coupon can only be used if the number of items exceeds <b>{0}</b>.',lowerThanCurrent:'This coupon offers a lower discount than the current <b>{0}</b>.',couponAccepted:'Coupon code accepted, discounting <b>{0}</b>.',success:'Success',reclaimed:'This coupon has already been used!',higherThanTotal:'The coupon provides a higher discount (<b>{0}</b>) than the cart total.',noSuch:'No such coupon exists!',cart:'Cart',view:'View',addSelectedItems:'Add selected items!',addThisItem:'Add this item!',shoppingCart:'Shopping cart',edit:'Edit',subtotal:'Subtotal',couponCode:'Coupon code',remove:'Remove',discount:'Discount',shippingAndHandling:'Shipping and handling',tax:'Tax',total:'Total',continueShopping:'Continue shopping',emptyCart:'Empty Cart',buyNow:'Buy Now',processedByPaypal:'Payment processed by PayPal',scrollTopTooltip:'Top',checkThisOut:'This album is worth checking out',copied:'Copied',copy:'Copy',email:'Email',today:'today',yesterday:'yesterday',yearsAgo:'{0} years ago',monthsAgo:'{0} months ago',daysAgo:'{0} days ago',inThePast24Hours:'in the past 24 hours',inThePast48Hours:'in the past 48 hours',inThePastNYears:'in the past {0} years',inThePastNMonths:'in the past {0} months',inThePastNDays:'in the past {0} days',close:'Close',cookiePolicyText:'This album is using cookies to remember your preferences. It does not track you, nor does it store personal data.',cookiePolicyAgree:'Got it',gdprComplianceText:'This site uses the following external services that may track y
vendor: 4,834 bytes, lines 3-6
3our visits anonymously. Disabling tracking will stop their functionality.',allowAll:'Allow all',denyAll:'Deny all',allowSelected:'Allow selected',cookiePolicyLearnMore:'Learn more',restrictedLinkTitle:'Restricted material',restrictedLinkQuestion:'Please verify your age to proceed!',restrictedLinkYes:'I´m over 18',restrictedLinkNo:'I´m below 18',return:'return',rendering:'Rendering',selectedItems:'Selected items',label:'Label',search:'Search',searchNew:'Search new',loading:'Loading',foundNTimes:'{0} image(s)',foundTooMany:'{0} images; displaying only the first {1}',notFound:'not found',searchPlaceholder:'Search...',more:'more',less:'less',imagesTaken:'Images taken',imagesModified:'Images modified',imagesAdded:'Images added',betweenDays:'between {0} and {1}',onDay:'on {0}',afterDay:'after {0}',beforeDay:'before {0}',sinceMyLastVisit:'since my last visit',results:'Results',sortedBy:'Sorted by',ascending:'ascending',descending:'descending',noRating:'No rating',multipleSelectHint:'Use SHIFT to select range, CTRL for multiple entries',select:'Select',reset:'Reset',sortBy:'Sort by',image:'image',images:'images',audio:'audio',audios:'audios',video:'video',videos:'videos',other:'other',others:'others',fatalError:'Fatal error!'};
4/*! modernizr 3.6.0 (Custom Build) | MIT *
5 * https://modernizr.com/download/?-adownload-backgroundsize-bgsizecover-cssanimations-cssgradients-csstransforms-csstransforms3d-csstransitions-cssvhunit-hiddenscroll-preserve3d-svg-touchevents-setclasses !*/
6!function(e,t,n){function r(t,n,r){var s;if("getComputedStyle"in e){s=getComputedStyle.call(e,t,n);var o=e.console;if(null!==s)r&&(s=s.getPropertyValue(r));else if(o){var i=o.error?"error":"log";o[i].call(o,"getComputedStyle returning null, its possible modernizr test results are inaccurate")}}else s=!n&&t.currentStyle&&t.currentStyle[r];return s}function s(e){var t=z.className,n=Modernizr._config.classPrefix||"";if(k&&(t=t.baseVal),Modernizr._config.enableJSClass){var r=new RegExp("(^|\\s)"+n+"no-js(\\s|$)");t=t.replace(r,"$1"+n+"js$2")}Modernizr._config.enableClasses&&(t+=" "+n+e.join(" "+n),k?z.className.baseVal=t:z.className=t)}function o(){return"function"!=typeof t.createElement?t.createElement(arguments[0]):k?t.createElementNS.call(t,"http://www.w3.org/2000/svg",arguments[0]):t.createElement.apply(t,arguments)}function i(e,t){return typeof e===t}function a(){var e,t,n,r,s,o,a;for(var l in C)if(C.hasOwnProperty(l)){if(e=[],t=C[l],t.name&&(e.push(t.name.toLowerCase()),t.options&&t.options.aliases&&t.options.aliases.length))for(n=0;n<t.options.aliases.length;n++)e.push(t.options.aliases[n].toLowerCase());for(r=i(t.fn,"function")?t.fn():t.fn,s=0;s<e.length;s++)o=e[s],a=o.split("."),1===a.length?Modernizr[a[0]]=r:(!Modernizr[a[0]]||Modernizr[a[0]]instanceof Boolean||(Modernizr[a[0]]=new Boolean(Modernizr[a[0]])),Modernizr[a[0]][a[1]]=r),S.push((r?"":"no-")+a.join("-"))}}function l(e,t){return e-1===t||e===t||e+1===t}function u(){var e=t.body;return e||(e=o(k?"svg":"body"),e.fake=!0),e}function d(e,n,r,s){var i,a,l,d,f="modernizr",c=o("div"),p=u();if(parseInt(r,10))for(;r--;)l=o("div"),l.id=s?s[r]:f+(r+1),c.appendChild(l);return i=o("style"),i.type="text/css",i.id="s"+f,(p.fake?p:c).appendChild(i),p.appendChild(c),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(t.createTextNode(e)),c.id=f,p.fake&&(p.style.background="",p.style.overflow="hidden",d=z.style.overflow,z.style.overflow="hidden",z.appendChild(p)),a=n(c,e),p.fake?(p.parentNode.removeChild(p),z.style.overflow=d,z.offsetHeight):c.parentNode.removeChild(c),!!a}function f(e,t){return!!~(""+e).indexOf(t)}function c(e,t){return function(){return e.apply(t,arguments)}}function p(e,t,n){var r;for(var s in e)if(e[s]in t)return n===!1?e[s]:(r=t[e[s]],i(r,"function")?c(r,n||t):r);return!1}function m(e){return e.replace(/([a-z])-([a-z])/g,function(e,t,n){return t+n.toUpperCase()}).replace(/^-/,"")}function g(e){return e.replace(/([A-Z])/g,function(e,t){return"-"+t.toLowerCase()}).replace(/^ms-/,"-ms-")}function h(t,s){var o=t.length;if("CSS"in e&&"supports"in e.CSS){for(;o--;)if(e.CSS.supports(g(t[o]),s))return!0;return!1}if("CSSSupportsRule"in e){for(var i=[];o--;)i.push("("+g(t[o])+":"+s+")");return i=i.join(" or "),d("@supports ("+i+") { #modernizr { position: absolute; } }",function(e){return"absolute"==r(e,null,"position")})}return n}function v(e,t,r,s){function a(){u&&(delete O.style,delete O.modElem)}if(s=i(s,"undefined")?!1:s,!i(r,"undefined")){var l=h(e,r);if(!i(l,"undefined"))return l}for(var u,d,c,p,g,v=["modernizr","tspan","samp"];!O.style&&v.length;)u=!0,O.modElem=o(v.shift()),O.style=O.modElem.style;for(c=e.length,d=0;c>d;d++)if(p=e[d],g=O.style[p],f(p,"-")&&(p=m(p)),O.style[p]!==n){if(s||i(r,"undefined"))return a(),"pfx"==t?p:!0;try{O.style[p]=r}catch(y){}if(O.style[p]!=g)return a(),"pfx"==t?p:!0}return a(),!1}
6function y(e,t,n,r,s){var o=e.charAt(0).toUpperCase()+e.slice(1),a=(e+" "+N.join(o+" ")+o).split(" ");return i(t,"string")||i(t,"undefined")?v(a,t,r,s):(a=(e+" "+j.join(o+" ")+o).split(" "),p(a,t,n))}function w(e,t,r){return y(e,n,n,t,r)}var S=[],C=[],x={_version:"3.6.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,t){var n=this;setTimeout(function(){t(n[e])},0)},addTest:function(e,t,n){C.push({name:e,fn:t,options:n})},addAsyncTest:function(e){C.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=x,Modernizr=new Modernizr,Modernizr.addTest("svg",!!t.createElementNS&&!!t.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect);var b=x._config.usePrefixes?" -webkit- -moz- -o- -ms- ".split(" "):["",""];x._prefixes=b;var T="CSS"in e&&"supports"in e.CSS,_="supportsCSS"in e;Modernizr.addTest("supports",T||_);var z=t.documentElement,k="svg"===z.nodeName.toLowerCase();Modernizr.addTest("cssgradients",function(){for(var e,t="background-image:",n="gradient(linear,left top,right bottom,from(#9f9),to(white));",r="",s=0,i=b.length-1;i>s;s++)e=0===s?"to ":"",r+=t+b[s]+"linear-gradient("+e+"left top, #9f9, white);";Modernizr._config.usePrefixes&&(r+=t+"-webkit-"+n);var a=o("a"),l=a.style;return l.cssText=r,(""+l.backgroundImage).indexOf("gradient")>-1}),Modernizr.addTest("preserve3d",function(){var t,n,r=e.CSS,s=!1;return r&&r.supports&&r.supports("(transform-style: preserve-3d)")?!0:(t=o("a"),n=o("a"),t.style.cssText="display: block; transform-style: preserve-3d; transform-origin: right; transform: rotateY(40deg);",n.style.cssText="display: block; width: 9px; height: 1px; background: #000; transform-origin: right; transform: rotateY(40deg);",t.appendChild(n),z.appendChild(t),s=n.getBoundingClientRect(),z.removeChild(t),s=s.width&&s.width<4)});var P=x.testStyles=d;Modernizr.addTest("touchevents",function(){var n;if("ontouchstart"in e||e.DocumentTouch&&t instanceof DocumentTouch)n=!0;else{var r=["@media (",b.join("touch-enabled),("),"heartz",")","{#modernizr{top:9px;position:absolute}}"].join("");P(r,function(e){n=9===e.offsetTop})}return n}),Modernizr.addTest("hiddenscroll",function(){return P("#modernizr {width:100px;height:100px;overflow:scroll}",function(e){return e.offsetWidth===e.clientWidth})}),P("#modernizr { height: 50vh; }",function(t){var n=parseInt(e.innerHeight/2,10),s=parseInt(r(t,null,"height"),10);Modernizr.addTest("cssvhunit",l(s,n))});var E="Moz O ms Webkit",N=x._config.usePrefixes?E.split(" "):[];x._cssomPrefixes=N;var j=x._config.usePrefixes?E.toLowerCase().split(" "):[];x._domPrefixes=j;var A={elem:o("modernizr")};Modernizr._q.push(function(){delete A.elem});var O={style:A.elem.style};Modernizr._q.unshift(function(){delete O.style}),x.testAllProps=y,x.testAllProps=w,Modernizr.addTest("backgroundsize",w("backgroundSize","100%",!0)),Modernizr.addTest("cssanimations",w("animationName","a",!0)),Modernizr.addTest("bgsizecover",w("backgroundSize","cover")),Modernizr.addTest("csstransforms",function(){return-1===navigator.userAgent.indexOf("Android 2.")&&w("transform","scale(1)",!0)}),Modernizr.addTest("csstransitions",w("transition","all",!0)),Modernizr.addTest("csstransforms3d",function(){return!!w("perspective","1px",!0)}),Modernizr.addTest("adownload",!e.externalHost&&"download"in o("a")),a(),s(S),delete x.addTest,delete x.addAsyncTest;for(var q=0;q<Modernizr._q.length;q++)Modernizr._q[q]();e.Modernizr=Modernizr}(window,document);
7/**
8 * what-input - A global utility for tracking the current input method (mouse, keyboard or touch).
9 * @version v5.2.1
10 * @link https://github.com/ten1seven/what-input
11 * @license MIT
12 */
13(function webpackUniversalModuleDefinition(root, factory) {
14	if(typeof exports === 'object' && typeof module === 'object')
15		module.exports = factory();
16	else if(typeof define === 'function' && define.amd)
17		define("whatInput", [], factory);
18	else if(typeof exports === 'object')
19		exports["whatInput"] = factory();
20	else
21		root["whatInput"] = factory();
22})(this, function() {
23return /******/ (function(modules) { // webpackBootstrap
24/******/ 	// The module cache
25/******/ 	var installedModules = {};
26
27/******/ 	// The require function
28/******/ 	function __webpack_require__(moduleId) {
29
30/******/ 		// Check if module is in cache
31/******/ 		if(installedModules[moduleId])
32/******/ 			return installedModules[moduleId].exports;
33
34/******/ 		// Create a new module (and put it into the cache)
35/******/ 		var module = installedModules[moduleId] = {
36/******/ 			exports: {},
37/******/ 			id: moduleId,
38/******/ 			loaded: false
39/******/ 		};
40
41/******/ 		// Execute the module function
42/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
43
44/******/ 		// Flag the module as loaded
45/******/ 		module.loaded = true;
46
47/******/ 		// Return the exports of the module
48/******/ 		return module.exports;
49/******/ 	}
50
51
52/******/ 	// expose the modules object (__webpack_modules__)
53/******/ 	__webpack_require__.m = modules;
54
55/******/ 	// expose the module cache
56/******/ 	__webpack_require__.c = installedModules;
57
58/******/ 	// __webpack_public_path__
59/******/ 	__webpack_require__.p = "";
60
61/******/ 	// Load entry module and return exports
62/******/ 	return __webpack_require__(0);
63/******/ })
64/************************************************************************/
65/******/ ([
66/* 0 */
67/***/ (function(module, exports) {
68
69	'use strict';
70
71	module.exports = function () {
72	  /*
73	   * bail out if there is no document or window
74	   * (i.e. in a node/non-DOM environment)
75	   *
76	   * Return a stubbed API instead
77	   */
78	  if (typeof document === 'undefined' || typeof window === 'undefined') {
79	    return {
80	      // always return "initial" because no interaction will ever be detected
81	      ask: function ask() {
82	        return 'initial';
83	      },
84
85	      // always return null
86	      element: function element() {
87	        return null;
88	      },
89
90	      // no-op
91	      ignoreKeys: function ignoreKeys() {},
92
93	      // no-op
94	      specificKeys: function specificKeys() {},
95
96	      // no-op
97	      registerOnChange: function registerOnChange() {},
98
99	      // no-op
100	      unRegisterOnChange: function unRegisterOnChange() {}
101	    };
102	  }
103
104	  /*
105	   * variables
106	   */
107
108	  // cache document.documentElement
109	  var docElem = document.documentElement;
110
111	  // currently focused dom element
112	  var currentElement = null;
113
114	  // last used input type
115	  var currentInput = 'initial';
116
117	  // last used input intent
118	  var currentIntent = currentInput;
119
120	  // UNIX timestamp of current event
121	  var currentTimestamp = Date.now();
122
123	  // check for sessionStorage support
124	  // then check for session variables and use if available
125	  try {
126	    if (window.sessionStorage.getItem('what-input')) {
127	      currentInput = window.sessionStorage.getItem('what-input');
128	    }
129
130	    if (window.sessionStorage.getItem('what-intent')) {
131	      currentIntent = window.sessionStorage.getItem('what-intent');
132	    }
133	  } catch (e) {}
134
135	  // form input types
136	  var formInputs = ['button', 'input', 'select', 'textarea'];
137
138	  // empty array for holding callback functions
139	  var functionList = [];
140
141	  // list of modifier keys commonly used with the mouse and
142	  // can be safely ignored to prevent false keyboard detection
143	  var ignoreMap = [16, // shift
144	  17, // control
145	  18, // alt
146	  91, // Windows key / left Apple cmd
147	  93 // Windows menu / right Apple cmd
148	  ];
149
150	  var specificMap = [];
151
152	  // mapping of events to input types
153	  var inputMap = {
154	    keydown: 'keyboard',
155	    keyup: 'keyboard',
156	    mousedown: 'mouse',
157	    mousemove: 'mouse',
158	    MSPointerDown: 'pointer',
159	    MSPointerMove: 'pointer',
160	    pointerdown: 'pointer',
161	    pointermove: 'pointer',
162	    touchstart: 'touch',
163	    touchend: 'touch'
164
165	    // boolean: true if the page is being scrolled
166	  };var isScrolling = false;
167
168	  // store current mouse position
169	  var mousePos = {
170	    x: null,
171	    y: null
172
173	    // map of IE 10 pointer events
174	  };var pointerMap = {
175	    2: 'touch',
176	    3: 'touch', // treat pen like touch
177	    4: 'mouse'
178
179	    // check support for passive event listeners
180	  };var supportsPassive = false;
181
182	  try {
183	    var opts = Object.defineProperty({}, 'passive', {
184	      get: function get() {
185	        supportsPassive = true;
186	      }
187	    });
188
189	    window.addEventListener('test', null, opts);
190	  } catch (e) {}
191
192	  /*
193	   * set up
194	   */
195
196	  var setUp = function setUp() {
197	    // add correct mouse wheel event mapping to `inputMap`
198	    inputMap[detectWheel()] = 'mouse';
199
200	    addListeners();
201	    doUpdate('input');
202	    doUpdate('intent');
203	  };
204
205	  /*
206	   * events
207	   */
208
209	  var addListeners = function addListeners() {
210	    // `pointermove`, `MSPointerMove`, `mousemove` and mouse wheel event binding
211	    // can only demonstrate potential, but not actual, interaction
212	    // and are treated separately
213	    var options = supportsPassive ? { passive: true } : false;
214
215	    // pointer events (mouse, pen, touch)
216	    if (window.PointerEvent) {
217	      window.addEventListener('pointerdown', setInput);
218	      window.addEventListener('pointermove', setIntent);
219	    } else if (window.MSPointerEvent) {
220	      window.addEventListener('MSPointerDown', setInput);
221	      window.addEventListener('MSPointerMove', setIntent);
222	    } else {
223	      // mouse events
224	      window.addEventListener('mousedown', setInput);
225	      window.addEventListener('mousemove', setIntent);
226
227	      // touch events
228	      if ('ontouchstart' in window) {
229	        window.addEventListener('touchstart', setInput, options);
230	        window.addEventListener('touchend', setInput);
231	      }
232	    }
233
234	    // mouse wheel
235	    window.addEventListener(detectWheel(), setIntent, options);
236
237	    // keyboard events
238	    window.addEventListener('keydown', setInput);
239	    window.addEventListener('keyup', setInput);
240
241	    // focus events
242	    window.addEventListener('focusin', setElement);
243	    window.addEventListener('focusout', clearElement);
244	  };
245
246	  // checks conditions before updating new input
247	  var setInput = function setInput(event) {
248	    var eventKey = event.which;
249	    var value = inputMap[event.type];
250
251	    if (value === 'pointer') {
252	      value = pointerType(event);
253	    }
254
255	    var ignoreMatch = !specificMap.length && ignoreMap.indexOf(eventKey) === -1;
256
257	    var specificMatch = specificMap.length && specificMap.indexOf(eventKey) !== -1;
258
259	    var shouldUpdate = value === 'keyboard' && eventKey && (ignoreMatch || specificMatch) || value === 'mouse' || value === 'touch';
260
261	    // prevent touch detection from being overridden by event execution order
262	    if (validateTouch(value)) {
263	      shouldUpdate = false;
264	    }
265
266	    if (shouldUpdate && currentInput !== value) {
267	      currentInput = value;
268
269	      try {
270	        window.sessionStorage.setItem('what-input', currentInput);
271	      } catch (e) {}
272
273	      doUpdate('input');
274	    }
275
276	    if (shouldUpdate && currentIntent !== value) {
277	      // preserve intent for keyboard interaction with form fields
278	      var activeElem = document.activeElement;
279	      var notFormInput = activeElem && activeElem.nodeName && formInputs.indexOf(activeElem.nodeName.toLowerCase()) === -1 || activeElem.nodeName.toLowerCase() === 'button' && !checkClosest(activeElem, 'form');
280
281	      if (notFormInput) {
282	        currentIntent = value;
283
284	        try {
285	          window.sessionStorage.setItem('what-intent', currentIntent);
286	        } catch (e) {}
287
288	        doUpdate('intent');
289	      }
290	    }
291	  };
292
293	  // updates the doc and `inputTypes` array with new input
294	  var doUpdate = function doUpdate(which) {
295	    docElem.setAttribute('data-what' + which, which === 'input' ? currentInput : currentIntent);
296
297	    fireFunctions(which);
298	  };
299
300	  // updates input intent for `mousemove` and `pointermove`
301	  var setIntent = function setIntent(event) {
302	    var value = inputMap[event.type];
303
304	    if (value === 'pointer') {
305	      value = pointerType(event);
306	    }
307
308	    // test to see if `mousemove` happened relative to the screen to detect scrolling versus mousemove
309	    detectScrolling(event);
310
311	    // only execute if scrolling isn't happening
312	    if (!isScrolling && !validateTouch(value) && currentIntent !== value) {
313	      currentIntent = value;
314
315	      try {
316	        window.sessionStorage.setItem('what-intent', currentIntent);
317	      } catch (e) {}
318
319	      doUpdate('intent');
320	    }
321	  };
322
323	  var setElement = function setElement(event) {
324	    if (!event.target.nodeName) {
325	      // If nodeName is undefined, clear the element
326	      // This can happen if click inside an <svg> element.
327	      clearElement();
328	      return;
329	    }
330
331	    currentElement = event.target.nodeName.toLowerCase();
332	    docElem.setAttribute('data-whatelement', currentElement);
333
334	    if (event.target.classList && event.target.classList.length) {
335	      docElem.setAttribute('data-whatclasses', event.target.classList.toString().replace(' ', ','));
336	    }
337	  };
338
339	  var clearElement = function clearElement() {
340	    currentElement = null;
341
342	    docElem.removeAttribute('data-whatelement');
343	    docElem.removeAttribute('data-whatclasses');
344	  };
345
346	  /*
347	   * utilities
348	   */
349
350	  var pointerType = function pointerType(event) {
351	    if (typeof event.pointerType === 'number') {
352	      return pointerMap[event.pointerType];
353	    } else {
354	      // treat pen like touch
355	      return event.pointerType === 'pen' ? 'touch' : event.pointerType;
356	    }
357	  };
358
359	  // prevent touch detection from being overridden by event execution order
360	  var validateTouch = function validateTouch(value) {
361	    var timestamp = Date.now();
362
363	    var touchIsValid = value === 'mouse' && currentInput === 'touch' && timestamp - currentTimestamp < 200;
364
365	    currentTimestamp = timestamp;
366
367	    return touchIsValid;
368	  };
369
370	  // detect version of mouse wheel event to use
371	  // via https://developer.mozilla.org/en-US/docs/Web/Events/wheel
372	  var detectWheel = function detectWheel() {
373	    var wheelType = void 0;
374
375	    // Modern browsers support "wheel"
376	    if ('onwheel' in document.createElement('div')) {
377	      wheelType = 'wheel';
378	    } else {
379	      // Webkit and IE support at least "mousewheel"
380	      // or assume that remaining browsers are older Firefox
381	      wheelType = document.onmousewheel !== undefined ? 'mousewheel' : 'DOMMouseScroll';
382	    }
383
384	    return wheelType;
385	  };
386
387	  // runs callback functions
388	  var fireFunctions = function fireFunctions(type) {
389	    for (var i = 0, len = functionList.length; i < len; i++) {
390	      if (functionList[i].type === type) {
391	        functionList[i].fn.call(undefined, type === 'input' ? currentInput : currentIntent);
392	      }
393	    }
394	  };
395
396	  // finds matching element in an object
397	  var objPos = function objPos(match) {
398	    for (var i = 0, len = functionList.length; i < len; i++) {
399	      if (functionList[i].fn === match) {
400	        return i;
401	      }
402	    }
403	  };
404
405	  var detectScrolling = function detectScrolling(event) {
406	    if (mousePos['x'] !== event.screenX || mousePos['y'] !== event.screenY) {
407	      isScrolling = false;
408
409	      mousePos['x'] = event.screenX;
410	      mousePos['y'] = event.screenY;
411	    } else {
412	      isScrolling = true;
413	    }
414	  };
415
416	  // manual version of `closest()`
417	  var checkClosest = function checkClosest(elem, tag) {
418	    var ElementPrototype = window.Element.prototype;
419
420	    if (!ElementPrototype.matches) {
421	      ElementPrototype.matches = ElementPrototype.msMatchesSelector || ElementPrototype.webkitMatchesSelector;
422	    }
423
424	    if (!ElementPrototype.closest) {
425	      do {
426	        if (elem.matches(tag)) {
427	          return elem;
428	        }
429
430	        elem = elem.parentElement || elem.parentNode;
431	      } while (elem !== null && elem.nodeType === 1);
432
433	      return null;
434	    } else {
435	      return elem.closest(tag);
436	    }
437	  };
438
439	  /*
440	   * init
441	   */
442
443	  // don't start script unless browser cuts the mustard
444	  // (also passes if polyfills are used)
445	  if ('addEventListener' in window && Array.prototype.indexOf) {
446	    setUp();
447	  }
448
449	  /*
450	   * api
451	   */
452
453	  return {
454	    // returns string: the current input type
455	    // opt: 'intent'|'input'
456	    // 'input' (default): returns the same value as the `data-whatinput` attribute
457	    // 'intent': includes `data-whatintent` value if it's different than `data-whatinput`
458	    ask: function ask(opt) {
459	      return opt === 'intent' ? currentIntent : currentInput;
460	    },
461
462	    // returns string: the currently focused element or null
463	    element: function element() {
464	      return currentElement;
465	    },
466
467	    // overwrites ignored keys with provided array
468	    ignoreKeys: function ignoreKeys(arr) {
469	      ignoreMap = arr;
470	    },
471
472	    // overwrites specific char keys to update on
473	    specificKeys: function specificKeys(arr) {
474	      specificMap = arr;
475	    },
476
477	    // attach functions to input and intent "events"
478	    // funct: function to fire on change
479	    // eventType: 'input'|'intent'
480	    registerOnChange: function registerOnChange(fn, eventType) {
481	      functionList.push({
482	        fn: fn,
483	        type: eventType || 'input'
484	      });
485	    },
486
487	    unRegisterOnChange: function unRegisterOnChange(fn) {
488	      var position = objPos(fn);
489
490	      if (position || position === 0) {
491	        functionList.splice(position, 1);
492	      }
493	    }
494	  };
495	}();
496
497/***/ })
498/******/ ])
499});
500;
501/*
502 * laza.util - miscellaneous utility functions and prototype extensions
503 */
504
505;
506
507// Ensuring no 'console is undefined' errors happen
508
509window.console = window.console || (function(){
510		return {
511			log: function(msg) {}
512		};
513	})();
514	
515// Ensuring log function exists
516var log = function() {};
517
518/*
519 *	Constants
520 */
521	 
522var		// Typeof check
523		UNDEF 			= 'undefined',
524		OBJECT			= 'object',
525		FUNCTION		= 'function',
526		STRING			= 'string',
527		NUMBER			= 'number',
528		BOOLEAN			= 'boolean',
529		
530		NOLINK 			= 'javascript:void(0)',
531		
532		LOCAL 			= document.location.protocol === 'file:',
533	
534		ONEDAY_S		= 60 * 60 * 24,
535		ONEDAY_MS		= 60 * 60 * 24 * 1000,
536	
537		STARS			= '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="80" height="16" viewB
537ox="0 0 80 16" xml:space="preserve"><path d="M12,10.094l0.938,5.5L8,13l-4.938,2.594L4,10.094L0,6.219l5.531-0.813l2.469-5l2.469,5L16,6.219L12,10.094z"/><path d="M28,10.094l0.938,5.5L24,13l-4.938,2.594l0.938-5.5l-4-3.875l5.531-0.813l2.469-5l2.469,5L32,6.219L28,10.094z"/><path d="M44,10.094l0.938,5.5L40,13l-4.938,2.594l0.938-5.5l-4-3.875l5.531-0.813l2.469-5l2.469,5L48,6.219L44,10.094z"/><path d="M60,10.094l0.938,5.5L56,13l-4.938,2.594l0.938-5.5l-4-3.875l5.531-0.813l2.469-5l2.469,5L64,6.219L60,10.094z"/><path d="M76,10.094l0.938,5.5L72,13l-4.938,2.594l0.938-5.5l-4-3.875l5.531-0.813l2.469-5l2.469,5L80,6.219L76,10.094z"/></svg>';
538	
539		DIR_PATH 		= (function() {												// Path to current folder on the site
540								let p = window.location.pathname,
541									level = document.getElementsByTagName('html')[0].getAttribute('data-level') || 0;
542								
543								for (let i = 0; i < level; i++) {
544									p = p.substring(0, p.lastIndexOf('/'));
545								}
546								
547								return p + '/';
548							})(),
549	
550		LOCALSTORAGE 	= (function() {												// Test for local storage
551								try {
552									localStorage.setItem('_t', UNDEF);
553									localStorage.removeItem('_t');
554									return true;
555								} catch(e) {
556									return false;
557								}
558							})(),
559							
560		HISTORY			= (function() {												// Test for history
561								// Taken from Modernizr 3.1
562								let ua = navigator.userAgent;
563						
564								if ((ua.indexOf('Android 2.') !== -1 ||
565									(ua.indexOf('Android 4.0') !== -1)) &&
566									ua.indexOf('Mobile Safari') !== -1 &&
567									ua.indexOf('Chrome') === -1 &&
568									ua.indexOf('Windows Phone') === -1) {
569									return false;
570								}
571						
572								return (window.history && 'pushState' in window.history);
573							})(),
574							
575		VEND 			= (function() {												// Browser vendor test
576								let ua = navigator.userAgent;
577								/*
578									PC:
579										IE 8: 		"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)"
580										IE 9: 		"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Tablet PC 2.0; .NET4.0C)"
581										IE 10: 		"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0; .NET4.0E; .NET4.0C)"
582										Edge:		"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299"
583										Opera 12: 	"Opera/9.80 (Windows NT 6.1; WOW64) Presto/2.12.388 Version/12.15"
584										Firefox 21: "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0"
585										Chrome 27: 	"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.94 Safari/537.36"
586									Mac:
587										Chrome 27: 	"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36"
588										Firefox 21: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0"
589										Safari 6: 	"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/536.26.14 (KHTML, like Gecko) Version/6.0.1 Safari/536.26.14"
590								 */
591								if (ua.indexOf('Trident') > 0 || ua.indexOf('Edge') > 0) {
592									return 'ms';
593								} else if (ua.indexOf('AppleWebKit') > 0) {
594									return 'webkit';
595								} else if (ua.indexOf('Gecko') > 0) {
596									return 'moz';
597								} else if (ua.indexOf('Presto') > 0) {
598									return 'o';
599								} else if (ua.indexOf('Blink') > 0) {
600									return 'webkit';
601								}
602								return '';		
603							})(),
604							
605		JAVAFXBROWSER	= navigator.userAgent.indexOf('JavaFX/14') > 0,
606							
607		BACKFACEBUG		= (function() {
608								var m = navigator.userAgent.match(/Edge\/(\d+\.\d+)/);
609								return m && parseInt(m[1]) <= 16;
610							})(),
611							
612		TOUCH			= (function() {												// Touch event naming
613	
614								if (/Trident|Edge/.test(navigator.userAgent)) {
615									// Setting MS events
616									if (window.navigator.pointerEnabled) {
617										return {
618											'START': 	'pointerdown',
619											'MOVE':		'pointermove',
620											'END':		'pointerup',
621											'CANCEL':	'pointercancel'
622										};
623									}
624									return {
625										'START': 	'MSPointerDown',
626										'MOVE':		'MSPointerMove',
627										'END':		'MSPointerUp',
628										'CANCEL':	'MSPointerCancel'
629									};
630								}
631							
632								return {
633									'START': 	'touchstart',
634									'MOVE':		'touchmove',
635									'END':		'touchend',
636									'CANCEL':	'touchcancel'
637								};
638							})(),
639		
640		TOUCHENABLED	= function() {												// Test for touch support
641		
642								if (/Trident/.test(navigator.userAgent)) {
643									return (typeof navigator['maxTouchPoints'] !== UNDEF && navigator.maxTouchPoints); // || /IEMobile/.test(navigator.userAgent);
644								} else if (/Edge/.test(navigator.userAgent)) {
645									return (scrollbarWidth() == 0);
646								} else if (/(Chrome|CriOS)/.test(navigator.userAgent)) {
647									return /Mobile/.test(navigator.userAgent) || 'ontouchstart' in window; 
648								}
649								return 'ontouchstart' in window;
650							}(),
651							
652		ISIOSDEVICE		= /^iP/.test(navigator.platform) || /^Mac/.test(navigator.platform) && navigator.maxTouchPoints && (navigator.maxTouchPoints > 4),
653		
654		ISSEPARATEHOVER = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream,
655		
656		SMALLSCREEN		= window.innerWidth <= 480 || window.innerHeight <= 480 || (window.innerWidth <= 640 && window.innerHeight <= 640),
657		
658		PIXELRATIO		= window.hasOwnProperty('devicePixelRatio')? window.devicePixelRatio : 1,
659		
660		ZOOM			= window.hasOwnProperty('visualViewport')? window.visualViewport.scale : 1,
661		
662		HIDPI			= PIXELRATIO > 1.9,
663							//matchMedia("(-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2), (min-resolution: 192dpi)").matches;
664							
665		HASPDFVIEWER	= navigator.pdfViewerEnabled,
666		
667		LANGUAGE		= (navigator.hasOwnProperty('languages')? navigator.languages[0] : navigator.language),
668		
669		WEBP_LOSSY		= false,													// WebP support detected asynchronously
670		WEBP_LOSSLESS	= false,
671		checkWebpSupport = function(test, callback) {
672								let img = new Image();
673								
674								img.onload = function () {
675										callback((img.width > 0) && (img.height > 0));
676									};
677									
678								img.onerror = function () {
679										callback(false);
680									};
681									
682								img.src = "data:image/webp;base64," + test;
683							};
684
685// Checking WebP support by loading minimal images 				
686checkWebpSupport('UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA', function(r) { WEBP_LOSSY = r; });
687checkWebpSupport('UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==', function(r) { WEBP_LOSSLESS = r; });
688	
689if (typeof DEBUG === UNDEF) {
690	var	DEBUG = false;
691}
692		
693if (typeof REL_PATH === UNDEF) {
694	var REL_PATH = '';
695}
696
697if (typeof PAGE_NAME === UNDEF) {
698	var	PAGE_NAME = 'index.html';
699}
700
701if (typeof THUMBS_DIR === UNDEF) {
702	var	THUMBS_DIR = 'thumbs';
703}
704
705if (typeof SLIDES_DIR === UNDEF) {
706	var	SLIDES_DIR = 'slides';
707}
708
709/*
710 *	Extending prototypes
711 */
712 
713if (!String.prototype.hasOwnProperty('trim')) {
714	
715	String.wsp = [];
716	String.wsp[0x0009] = true;
717	String.wsp[0x000a] = true;
718	String.wsp[0x000b] = true;
719	String.wsp[0x000c] = true;
720	String.wsp[0x000d] = true;
721	String.wsp[0x0020] = true;
722	String.wsp[0x0085] = true;
723	String.wsp[0x00a0] = true;
724	String.wsp[0x1680] = true;
725	String.wsp[0x180e] = true;
726	String.wsp[0x2000] = true;
727	String.wsp[0x2001] = true;
728	String.wsp[0x2002] = true;
729	String.wsp[0x2003] = true;
730	String.wsp[0x2004] = true;
731	String.wsp[0x2005] = true;
732	String.wsp[0x2006] = true;
733	String.wsp[0x2007] = true;
734	String.wsp[0x2008] = true;
735	String.wsp[0x2009] = true;
736	String.wsp[0x200a] = true;
737	String.wsp[0x200b] = true;
738	String.wsp[0x2028] = true;
739	String.wsp[0x2029] = true;
740	String.wsp[0x202f] = true;
741	String.wsp[0x205f] = true;
742	String.wsp[0x3000] = true;
743	
744	String.prototype.trim = function() { 
745			var str = this + '', 
746				j = str.length;
747			if (j) {
748				var ws = String.wsp, 
749					i = 0;
750				--j;
751				while (j >= 0 && ws[str.charCodeAt(j)]) {
752					--j;
753				}
754				++j;
755				while (i < j && ws[str.charCodeAt(i)]) { 
756					++i; 
757				}
758				str = str.substring(i, j);
759			}
760			return str;
761		};
762}
763
764if (!String.prototype.hasOwnProperty('trunc')) {
765
766	String.prototype.trunc = function( n ) {
767			var t = this + '';
768			
769			if (t.length <= n) {
770				return t.toString();
771			}
772			
773			var s = t.substring(0, n - 1), 
774				i = s.lastIndexOf(' ');
775				
776			return ((i > 6 && (s.length - i) < 20)? s.substring(0, i) : s) + '...';
777		};
778}
779
780if (!String.prototype.hasOwnProperty('startsWith')) {
781
782	String.prototype.startsWith = function( s ) {
783			return (this + '').substring(0, s.length) === s;
784		};
785}
786
787if (!String.prototype.hasOwnProperty('endsWith')) {
788
789	String.prototype.endsWith = function( s ) {
790			return (this + '').substring(this.length - s.length) === s;
791		};
792}
793
794	String.prototype.capitalize = function() {
795			return this.charAt(0).toUpperCase() + this.slice(1);
796		};
797	
798	String.prototype.unCamelCase = function() {
799			return this.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
800		};
801	
802	String.prototype.getExt = function() {
803			var t = this + '', 
804				i = t.lastIndexOf('.');
805			return (i <= 0 || i >= t.length - 1)? '' : t.substring(i + 1);
806		};
807		
808	String.prototype.stripExt = function() {
809			var t = this + '',
810				i = t.lastIndexOf('.');
811			return (i <= 0 || i > t.length - 1)? t : t.substring(0, i);
812		};
813	
814	String.prototype.hasExt = function(x) {
815			var t = (this + ''), 
816				i = t.lastIndexOf('.');
817			if (i >= 0) {
818				t = t.substring(i + 1).toLowerCase();
819				return (x + ',').indexOf(t + ',') >= 0;
820			}
821			return !1;
822		};
823	
824	String.prototype.replaceExt = function( s ) {
825			var t = this + '', 
826				i = t.lastIndexOf('.');
827			return (i <= 0)? t : (t.substring(0, i + 1) + s);  
828		};
829	
830	String.prototype.fixExtension = function() {
831			return (this + '').replace(/.gif$/gi, '.png').replace(/.tif+$/gi, '.jpg');
832		};
833	
834	String.prototype.getDir = function() {
835			var u = (this + '').split('#')[0];
836			return u.substring(0, u.lastIndexOf('/') + 1);
837		};
838	
839	String.prototype.getFile = function() {
840			var u = (this + '').split('#')[0];
841			return u.substring(u.lastIndexOf('/') + 1);
842		};
843	
844	String.prototype.getRelpath = function(level) {
845			var t = (this + ''), 
846				i = t.lastIndexOf('#');
847			if (i === -1) {
848				i = t.length - 1;
849			} else {
850				i--;
851			}
852			for (; i >= 0; i--) {
853				if (t[i] === '/' && (level--) === 0)
854					break;
855			}
856			return t.substring(i + 1);
857		};
858	
859	String.prototype.fixUrl = function() {
860			var i, 
861				j, 
862				t = this + '';
863			while ((i = t.indexOf('../')) > 0) {
864				if (i === 1 || (j = t.lastIndexOf('/', i - 2)) === -1) {
865					return t.substring(i + 3);
866				}
867				t = t.substring(0, j) + t.substring(i + 2);
868			}
869			return t;
870		};
871	
872	String.prototype.fullUrl = function() {
873			var t = this + '';
874			if (!t.match(/^(http|ftp|file)/)) {
875				t = window.location.href.getDir() + t;
876			}
877			return t.fixUrl();
878		};
879	
880	String.prototype.cleanupHTML = function() {
881			var htmlregex = [
882					[ /<(b|h)r\/?>/gi, '\n' ],
883					[ /\&amp;/g, '&' ],
884					[ /\&nbsp;/g, ' ' ],
885					[ /\&lt;/g, '<' ],
886					[ /\&gt;/g, '>' ],
887					[ /\&(m|n)dash;/g , '-' ],
888					[ /\&apos;/g, '\'' ],
889					[ /\&quot;/g, '"' ]
890				],
891				t = this + '';
892				
893			for (var i = htmlregex.length - 1; i >= 0; i--) {
894				t = t.replace(htmlregex[i][0], htmlregex[i][1]);
895			}
896			
897			return t; 
898		};
899	
900	String.prototype.stripHTML = function(format) { 
901			var s = (this + '');
902			
903			if (format) {
904				s = s.cleanupHTML();
905			}
906			
907			return s.replace(/<\/?[^>]+>/g, ' '); 
908		};
909	
910	String.prototype.stripQuote = function() {
911			return (this + '').replace(/\"/gi, '&quot;');
912		};
913	
914	String.prototype.appendSep = function(s, sep) {
915			return (this.length? (this + (sep || ' &middot; ')) : '') + s; 
916		};
917	
918	String.prototype.rgb2hex = function() {
919			var t = this + '';
920			if (t.charAt(0) === '#' || t === 'transparent') {
921				return t;
922			}
923			var n, r = t.match(/\d+/g), h = '';
924			if ( r ) {
925				for (var i = 0; i < r.length && i < 3; ++i) {
926					n = parseInt( r[i], 10 ).toString(16);
927					h += ((n.length < 2)? '0' : '') + n;
928				}
929				return '#' + h;
930			}
931			return 'transparent';
932		};
933	
934	String.prototype.template = function(s) {
935			if (typeof s === UNDEF || !this) {
936				return this;
937			}
938			if (!isNaN(parseFloat(s)) && isFinite(s)) {
939				s = s + '';
940			}
941			var t = this + '';
942			if (s.constructor === Array) {
943				for (var i = 0; i < s.length; ++i) {
944					t = t.replace(new RegExp('\\{' + i + '\\}', 'gi'), s[i]);
945				}
946			} else {
947				t = t.replace( /\{0\}/gi, s );
948			}
949			return t;
950		};
951	
952	String.prototype.getSearchTerms = function() {
953			var t = this + '';
954			
955			if (t.indexOf('"') === -1) {
956				return t.split(' ');
957			} else {
958				var a = [],
959					i;
960			
961				do {
962					if ((i = t.indexOf('"')) > 0) {
963						a.push.apply(a, t.substring(0, i).split(' '));
964					}
965					t = t.substring(i + 1);
966					i = t.indexOf('"');
967					if (i < 0) {
968						a.push(t);
969						break;
970					}
971					a.push(t.substring(0, i));
972					t = t.substring(i + 1);
973					
974				} while (t.length);
975			
976				return a;
977			}
978		};
979	
980	String.prototype.objectify = function() {
981			
982			if (!this || !this.length) {
983				return this;
984			}
985			
986			var t = this + '';
987			
988			if (t.charAt(0) === '?' || t.charAt(0) === '#') {
989				t = t.substring(1);
990			}
991			
992			var r = {}, 
993				o, 
994				os = t.split('&');
995				
996			for (var i = 0, l = os.length; i < l; ++i) {
997				o = os[i].split('=');
998				if (o.length > 1) {
999					// Assing value
1000					if (o[0] === 'img') {
1001						// No decoding for image name
1002						r.img = o[1];
1003					} else {
1004						r[o[0]] = decodeURIComponent(o[1]);
1005					}
1006				} else  {
1007					// No value provided
1008					r[o[0]] = null;
1009				}
1010			}
1011			return r;
1012		};
1013	
1014	// Test if 's' is in the string
1015	String.prototype.testIn = function(s) {
1016			if (typeof s !== 'string') {
1017				s = s + '';
1018			}
1019			return (new RegExp(this, 'i')).test(s);
1020		};
1021	
1022	// Testing exact match
1023	String.prototype.testExactMatch = function(s) {
1024			if (s.constructor !== Array) {
1025				return this == s + '';
1026			} else {
1027				for (var i = 0, l = s.length; i < l; ++i) {
1028					if (this == s[i]) {
1029						return true;
1030					}
1031				}
1032			}
1033			return false;
1034		};
1035	
1036	// Fixing jAlbum paths
1037	String.prototype.fixjAlbumPaths = function(resPath, rootPath, relPath) {
1038			var s = (this + '').replace(/\%24/g, '\$');
1039			
1040			if (resPath) {
1041				s = s.replace(/\${resPath}/g, resPath);
1042			}
1043			if (rootPath) {
1044				s = s.replace(/\${rootPath}/g, rootPath);
1045			}
1046			if (relPath) {
1047				s = s.replace(/\${relPath}/g, relPath);
1048			}
1049			return s;
1050		};		
1051	
1052	// Ignore case but exact match
1053	String.prototype.testMatch = function(s) {
1054			if (typeof s === UNDEF) {
1055				return false;
1056			}
1057			
1058			var t = this.toLowerCase(); 
1059			
1060			if (s.constructor !== Array) {
1061				return t === (s + '').toLowerCase();
1062			} else {
1063				for (var i = 0, l = s.length; i < l; ++i) {
1064					if (t === s[i].toLowerCase()) {
1065						return true;
1066					}
1067				}
1068			}
1069			
1070			return false;
1071		};
1072	
1073	/* Search term in string (case insensitive) :: searching terms only in word beginnings
1074	 		this 		= target string, 
1075			s 			= serch term(s), 
1076			exact 		= matches word boundaries, 
1077			conj		= all terms must match (default: match any)
1078	 */
1079	
1080	String.prototype.searchTerm = function(s, exact, conj, caseSensitive) {
1081			if (typeof s === UNDEF || !this.length) {
1082				return false;
1083			}
1084		
1085			var cs  = (typeof caseSensitive !== UNDEF) ? caseSensitive : false,
1086				t   = cs ? (this + '') : (this + '').toLowerCase(),
1087				exact = (typeof exact !== UNDEF) ? exact : false,
1088				conj  = (typeof conj  !== UNDEF) ? conj  : false,
1089		
1090				// Split target into word tokens once
1091				tWords = t.split(/\W+/).filter(Boolean),
1092		
1093				_find = function(term) {
1094					term = cs ? term : term.toLowerCase();
1095					if (exact) {
1096						return tWords.some(function(w) { return w === term; });
1097					} else {
1098						// Match any word in t that *starts with* term
1099						return tWords.some(function(w) { return w.indexOf(term) === 0; });
1100					}
1101				};
1102		
1103			// Normalise s into an array of terms
1104			if (!Array.isArray(s)) {
1105				s = s.split(/\W+/).filter(Boolean);
1106			} else {
1107				// Flatten: each element may itself contain separators
1108				s = s.reduce(function(acc, item) {
1109					return acc.concat(item.split(/\W+/).filter(Boolean));
1110				}, []);
1111			}
1112		
1113			if (conj) {
1114				return s.every(function(term) { return _find(term); });
1115			} else {
1116				return s.some(function(term) { return _find(term); });
1117			}
1118		};
1119		
1120	// Creating hash code
1121	String.prototype.hashCode = function() {
1122			for (var h = 0, i = 0, l = this.length; i < l; ++i) {
1123				h = (h << 5) - h + this.charCodeAt(i);
1124				h &= h;
1125			}
1126			return h;
1127		};
1128	
1129	// > Min && < Max
1130	Math.minMax = function(a, b, c) {
1131			if (typeof a === STRING) a = parseFloat(a);
1132			if (typeof b === STRING) b = parseFloat(b);
1133			if (typeof c === STRING) c = parseFloat(c);
1134			return  (b < a)? a : ((b > c)? c : b); 
1135		};
1136		
1137	// Gets range
1138	Math.getRange = function(a, r) {
1139			if (r.constructor !== Array) {
1140				return (a >= r)? 1 : 0;
1141			}
1142			if (r.length === 1) {
1143				return (a >= r[0])? 1 : 0;
1144			}
1145			if (a < r[0]) {
1146				return 0;
1147			}
1148			for (var i = 1; i < r.length; i++) {
1149				if (a >= r[i - 1] && a < r[i]) {
1150					break;
1151				}
1152			}
1153			return i;
1154		};
1155
1156/*
1157 *	New functions and variables for the global context - no jQuery depen
1157dency
1158 */
1159
1160var encodeJ = [];
1161	encodeJ[33] 	= '%21'; 	// !
1162	encodeJ[35] 	= '%23';	// #
1163	encodeJ[36] 	= '%24';	// $
1164	encodeJ[38] 	= '%26';	// &
1165	encodeJ[39] 	= '%27';	// '
1166	encodeJ[40] 	= '%28';	// (
1167	encodeJ[41] 	= '%29';	// )
1168	encodeJ[43] 	= '%2B';	// +
1169	encodeJ[44] 	= '%2C';	// ,
1170	encodeJ[59] 	= '%3B';	// ;
1171	encodeJ[60] 	= '%3C';	// <
1172	encodeJ[61] 	= '%3D';	// =
1173	encodeJ[62] 	= '%3E';	// >
1174	encodeJ[63] 	= '%3F';	// ?
1175	encodeJ[64] 	= '%40';	// @
1176	encodeJ[123] 	= '%7B';	// {
1177	encodeJ[124]	= '%7C';	// |
1178	encodeJ[125]	= '%7D';	// }
1179	
1180var	transCodeJ = [];
1181	transCodeJ[33] 	= '%21'; 	// !
1182	transCodeJ[39] 	= '%27';	// '
1183	transCodeJ[40] 	= '%28';	// (
1184	transCodeJ[41] 	= '%29';	// )
1185	
1186
1187	// Unique ID generator 
1188var	uidCnt = 0,
1189	getUniqueId = function(pfx) {
1190			return (pfx || 'uid') + '_' + (++uidCnt);
1191		},
1192	
1193	// Object is empty?
1194	
1195	isEmpty = function(o) {
1196	
1197			if (o == null) {
1198				return true;
1199			}
1200			return (Object.getOwnPropertyNames(o)).length === 0;
1201		},
1202	
1203	// Parametrize
1204	
1205	paramize = function(o) {
1206		
1207			if (typeof o === 'number') {
1208				return '' + o;
1209			} else if (typeof o === 'string') {
1210				return o;
1211			} else if (typeof o === 'object') {
1212				// 1 level depth only
1213				var s = '',
1214					op = Object.getOwnPropertyNames(o),
1215					ol = op.length; 
1216				for (var i = 0; i < ol; i++) {
1217					if (o[op[i]] !== null) {
1218						if (op[i] === 'img') {
1219							s += '&img=' + o[op[i]];
1220						} else {
1221							s += '&' + op[i] + '=' + encodeURIComponent(o[op[i]]);
1222						}
1223					}
1224				}
1225				if (s.length) {
1226					return s.substring(1);
1227				}
1228			}
1229			return '';
1230		},
1231	
1232	// All elements are true in an array
1233	
1234	allTrue = function(o) {
1235		
1236			if (o && o.constructor === Array) {
1237				for (var i = 0; i < o.length; ++i) {
1238					if (!o[i]) {
1239						return false;
1240					}
1241				}
1242				return true;
1243			}
1244			return o === true;
1245		},
1246	
1247	// Remove empty strings from array
1248	
1249	removeEmpty = function(a) {
1250		
1251			if (a && a.constructor === Array) {
1252				var b = new Array(),
1253					i;
1254					
1255				for (i = 0; i < a.length; i++) {
1256					if (typeof a[i] === STRING && a[i].length) {
1257						b.push(a[i]);
1258					}
1259				}
1260				
1261				return b;
1262			}
1263			
1264			return a;
1265		},
1266
1267	// Get event coordinates
1268	
1269	getCoords = function(e) {
1270				var x, y;
1271				
1272				if (!e.touches) {
1273					// No touch
1274					return { 
1275						x: e.clientX, 
1276						y: e.clientY 
1277					};
1278				} else if (e.touches.length == 1) {
1279					return {
1280						x: e.touches[0].clientX,
1281						y: e.touches[0].clientY
1282					}
1283				} else if (e.changedTouches && e.changedTouches.length == 1) {
1284					return {
1285						x: e.changedTouches[0].clientX,
1286						y: e.changedTouches[0].clientY
1287					}
1288				}
1289				return null;
1290			},
1291		
1292	/*
1293	 *	Translate function
1294	 *	returns a translated key, or the default value (if exists), or the key itself de-camelcased
1295	 */
1296	
1297	translate = function(key, def) {
1298		
1299			key = key.trim();
1300			
1301			if (typeof Texts !== UNDEF) {
1302				if (Texts.hasOwnProperty(key)) {
1303					return Texts[key];
1304				}
1305			}
1306			
1307			if (typeof def !== UNDEF) {
1308				// Using the default
1309				if (DEBUG && console) {
1310					console.log('Using default translation: '+key+'='+def);
1311				}
1312				return def;
1313			}
1314			
1315			if (DEBUG && console) {
1316				console.log('Missing translation: '+key);
1317			}
1318			
1319			var s = key.replace(/([A-Z])/g, ' $1').toLowerCase();
1320			
1321			return s.charAt(0).toUpperCase() + s.slice(1);
1322		},
1323	
1324	/* 
1325	 *	Simpler method 
1326	 *	text = getKeys('key1,key2,key3', [defaults])
1327	 */
1328	
1329	getKeys = function(keys, def) {
1330			var t = {}, i, k = keys.split(','), l = k.length;
1331			
1332			for (i = 0; i < l; i++) {
1333				t[k[i]] = translate(k[i], def[k[i]]);
1334			}
1335			
1336			return t;
1337		},
1338	
1339	/*
1340	 *	Finds translation for each key in def Object
1341	 *	def should contain only elements that need translation
1342	 */
1343	
1344	getTranslations = function(def) {
1345			var t = {}, k;
1346			
1347			for (k in def) {
1348				if (typeof def[k] === 'object') {
1349					t[k] = getTranslations(def[k]);
1350				} else {
1351					t[k] = translate(k, def[k]);
1352				}
1353			}
1354			
1355			return t;
1356		},
1357	
1358	// Reading keys: k="name1,name2,... from attr="data-k" into m
1359	
1360	readData = function(el, k) {
1361			var o = {};
1362			
1363			if (el && el.length && k) {
1364				k = k.split(',');
1365				var v;
1366				for (var i = 0; i < k.length; i++) {
1367					if ((v = el.data(k[i])) != null) {
1368						o[k[i]] = v;
1369					}
1370				}
1371			}
1372			return o;
1373		},
1374		
1375	/*
1376	 *  Nice float numbers
1377	 */
1378	
1379	niceByte = function(val) {
1380			var v = parseFloat(val),  
1381				va = Math.abs(v),
1382				round = function(v) {
1383						return (v < 10)? v.toFixed(1) : Math.round(v);
1384					};
1385					
1386			if (va < 0.0001) {
1387				return '0';
1388			} else if (va < 0.1) {
1389				return v.toFixed(3);
1390			} else if (va < 1) {
1391				return v.toFixed(2);
1392			} else if (va < 1000) {
1393				return round(v);
1394			} else if (va < 1000000) {
1395				return round(v / 1000) + 'k';
1396			} else if (va < 1000000000) {
1397				return round(v / 1000000) + 'M';
1398			} else if (va < 1000000000000) {
1399				return round(v / 1000000000) + 'G';
1400			}
1401			
1402			return v.toExponential();
1403		},
1404				
1405	/*
1406	 *  Nice time formatting
1407	 */
1408	
1409	niceTime = function(val) {
1410			var v = parseFloat(val),  
1411				va = Math.abs(v),
1412				s,
1413				m,
1414				addZero = function(v) {
1415						return (v < 10)? ('0' + v) : ('' + v);
1416					},
1417				round = function(v) {
1418						return (v < 10)? v.toFixed(1) : Math.round(v);
1419					};
1420					
1421			if (va < 0.000001) {
1422				// Don't care about smaller
1423				return '0s';
1424			} else if (va < 0.001) {
1425				// microseconds
1426				return round(v * 1000000)  + '&#181;s';
1427			} else if (va < 1) {
1428				// milliseconds
1429				return round(v * 1000) + 'ms';
1430			} else if (va < 60) {
1431				// secoonds
1432				return round(v) + 's';
1433			} else {
1434				// hh mm ss
1435				s = Math.round(va) % 60;
1436				m = Math.floor(va) / 60 % 60;
1437				if (va < 3600) {
1438					return m + 'm' + addZero(s) + 's';
1439				}
1440				return Math.floor(va / 3600) + 'h' + addZero(m) + 'm' + addZero(s) + 's';
1441			} 
1442		},
1443				
1444	/*
1445	 *	Currency
1446	 */
1447	 
1448	toCurrency = function(n, curr) {
1449			var v = parseFloat(n);
1450				
1451			return v.toLocaleString(LANGUAGE, {
1452					style: 		'currency',
1453					currency: 	curr || 'EUR',
1454				});
1455		},
1456
1457	/*
1458	 *  Relative date
1459	 */
1460	
1461	getRelativeDate = function(days) {
1462		
1463			if (!days) {
1464				return translate('today');
1465			}
1466			
1467			if (days === 1) {
1468				return translate('yesterday');
1469			}
1470			
1471			var s, n;
1472			
1473			if (days >= 730) {
1474				s = translate('yearsAgo');
1475				n = Math.round(days / 365);
1476			} else if (days >= 60) {
1477				s = translate('monthsAgo');
1478				n = Math.round(days / 30.5);
1479			} else {
1480				s = translate('daysAgo');
1481				n = days;
1482			}
1483			
1484			return s.replace('{0}', n);
1485		},
1486	
1487	/*
1488	 *  Timespan
1489	 */
1490	
1491	getTimespan = function(days) {
1492		
1493			if (!days) {
1494				return translate('inThePast24Hours');
1495			}
1496			
1497			if (days < 2) {
1498				return translate('inThePast48Hours');
1499			}
1500				
1501			var s, n;
1502			
1503			if (days >= 730) {
1504				s = translate('inThePastNYears');
1505				n = Math.round(days / 365);
1506			} else if (days >= 60) {
1507				s = translate('inThePastNMonths');
1508				n = Math.round(days / 30.42);
1509			} else {
1510				s = translate('inThePastNDays');
1511				n = Math.round(days);
1512			}
1513			
1514			return s.replace('{0}', n);
1515		},
1516		
1517	/*
1518	 *  Reads float from a fraction notation
1519	 */
1520	
1521	getFractionValue = function(s) {
1522			var val = s.match(/^([\d\.]+)\/([\d\.]+)/);
1523
1524			if (val && val.length > 1 && parseFloat(val[2]) !== 0) {
1525				return val[1] / val[2];
1526			}
1527			
1528			return s;
1529		},
1530		
1531	/*
1532	 *  Converts exposure text to ms
1533	 */
1534	
1535	getExposure = function(s) {
1536		
1537			s = s.includes('/')? getFractionValue(s) : parseFloat(s);
1538			
1539			return s;
1540		},
1541		
1542	/*
1543	 *	Relative path:: path/to/folder/ or ../../path/to/folder/
1544	 *	always ends with '/' unless current folder
1545	 */
1546	 
1547	getRelativePath = function(from, to) {
1548		
1549			if (typeof to === UNDEF || typeof from === UNDEF) {
1550				// invalid
1551				return '';
1552			}
1553			
1554			if (from === './') {
1555				// Current folder
1556				from = REL_PATH;
1557			}
1558			
1559			if (to === './') {
1560				// Current folder
1561				to = REL_PATH;
1562			}
1563			
1564			if (!to.endsWith('/')) {
1565				to += '/';
1566			}
1567			
1568			if (!from.endsWith('/')) {
1569				from += '/';
1570			}
1571			
1572			if (from === to) {
1573				// Same folder
1574				return '';
1575			}
1576			
1577			if (from === '/') {
1578				// From root
1579				return to;
1580			}
1581			
1582			var fp = from.split('/').filter(Boolean);
1583			
1584			if (to === '/') {
1585				// To root
1586				return getParentFolderLink(fp.length);
1587			}
1588			
1589			// Generic case
1590			var tp = to.split('/').filter(Boolean);
1591			
1592			while (fp.length && tp.length && fp[0] === tp[0]) {
1593				fp.shift();
1594				tp.shift();
1595			}
1596			
1597			return getParentFolderLink(fp.length) + (tp.length? (tp.join('/') + '/') : '');
1598		},
1599
1600	getParentFolderLink = function(depth) {
1601			return (depth > 0)? '../../../../../../../../../../../../../../../../../../../../'.substring(0, 3 * depth) : '';
1602		},
1603	
1604	/*
1605	 *	Encode the same as Java
1606	 */
1607	
1608	encodeAsJava = function(s) {
1609			var r = '',
1610				ap = /^(https?:|file:)?\/\//i;
1611			
1612			s = ap.test(s)? encodeURI(s) : encodeURIComponent(s);
1613			for (var i = 0; i < s.length; i++) {
1614				r += encodeJ[s.charCodeAt(i)] || s.charAt(i);
1615			}
1616			
1617			return r;
1618		},
1619	
1620	/*
1621	 *	Encode the same as Java
1622	 */
1623	
1624	transcodeJavaURIComponent = function(s) {
1625			var r = '';
1626			
1627			for (var i = 0; i < s.length; i++) {
1628				r += transCodeJ[s.charCodeAt(i)] || s.charAt(i);
1629			}
1630			
1631			return r;
1632		},
1633		
1634
1635	/* 
1636	 *	Pure JS shallow extend function
1637	 */
1638	
1639	extend = function() {
1640		
1641			if (arguments.length < 2) {
1642				return arguments[0] || {};
1643			}
1644			
1645			var r = arguments[0];
1646			
1647			for (var i = 1; i < arguments.length; i++) {
1648				if (arguments[i] != null) { 
1649					for (var key in arguments[i]) {
1650						if (arguments[i].hasOwnProperty(key)) {
1651							r[key] = arguments[i][key];
1652						}
1653					}
1654				}
1655			}
1656			
1657			return r;
1658		},
1659	
1660	/*
1661	 *	Passing defaults to libraries
1662	 */
1663	 
1664	passDefaults = function(src, dst, props) {
1665		
1666			if (src && dst) {
1667				if (typeof props !== UNDEF) {
1668					// Copy the requested properties
1669					props = props.split(',');
1670					
1671					for (var i = 0; i < props.length; i++) {
1672						if (src.hasOwnProperty(props[i])) {
1673							dst[props[i]] = src[props[i]];
1674						}
1675					}
1676				} else {
1677					// Copy all
1678					for (var prop in src) {
1679						dst[prop] = src[prop];
1680					}
1681				}
1682			}
1683		},
1684	
1685	/*
1686	 *	Reading user preferences from cookies
1687	 */
1688	 
1689	readUserPrefs = function(dst, props) {
1690		
1691			if (typeof props !== UNDEF && dst) {
1692				props = props.split(',');
1693				
1694				var p;
1695				
1696				for (var i = 0; i < props.length; i++) {
1697					if ((p = $.cookie(props[i])) !== null) {
1698						dst[props[i]] = p;
1699					}
1700				}
1701			}
1702		 },
1703	
1704	// Saving preferences
1705	
1706	savePrefs = function(settings, prefs, name) {
1707			var pref = {},
1708				n;
1709				
1710			for (var i = 0; i < prefs.length; i++) {
1711				
1712				n = prefs[i];
1713				
1714				if (settings.hasOwnProperty(n)) {
1715					pref[n] = settings[n];
1716				}
1717			}
1718			
1719			if (pref) {
1720				$.cookie(typeof name === UNDEF? 'lb_pref' : name, pref);
1721			}
1722		},
1723		
1724	// Loading preferences
1725	
1726	loadPrefs = function(settings, prefs, name) {
1727			var pref = $.cookie(typeof name === UNDEF? 'lb_pref' : name),
1728				p,
1729				n;
1730				
1731			if (pref) {
1732				
1733				for (p in prefs) {
1734					
1735					n = prefs[p];
1736					
1737					if (pref.hasOwnProperty(n)) {
1738						settings[n] = (pref[n] === 'true' || pref[n] === 'false')? !!pref[n] : pref[n];
1739					}
1740				}
1741			}
1742		},
1743		
1744	/*
1745	 *	History management
1746	 */
1747	 
1748	 // Adds one new state component
1749	 
1750	 addParam = function(indexName, params, title) {
1751	
1752			if (HISTORY) {
1753				var hash = window.location.hash;
1754				
1755				if (hash) {
1756					// Already has hash, adding to params
1757					if (history.state) {
1758						if (typeof history.state === STRING) {
1759							history.state = history.state.objectify();
1760						}
1761						if (history.state.hasOwnProperty('img')) {
1762							history.state.img = encodeURIComponent(history.state.img);
1763						}
1764						params = extend(history.state, params);
1765					} else {
1766						if (hash.charAt(0) === '#') {
1767							hash = hash.substring(1);
1768						}
1769						params = extend(hash.objectify(), params);
1770					}
1771				}
1772				
1773				hash = '#' + paramize(params);
1774				
1775				if (hash !== window.location.hash) {
1776					history.pushState(params, (typeof title === UNDEF)? '' : title, (hash.length > 1)? hash : (indexName || 'index.html'));
1777				}
1778			}
1779		},
1780	
1781	// Sets a new state
1782	
1783	setParam = function(indexName, params, title) {
1784	
1785			if (HISTORY) {
1786				var hash = '#' + paramize(params);
1787				
1788				if (hash !== window.location.hash) {
1789					history.pushState(params, (typeof title === UNDEF)? '' : title, (hash.length > 1)? hash : (indexName || 'index.html'));
1790				}
1791			}
1792		},
1793
1794	// Removes one parameter (String)
1795	
1796	removeParam = function(indexName, param, title) {
1797		
1798			if (HISTORY) {
1799				var hash = window.location.hash,
1800					params;
1801				
1802				if (hash) {
1803					if (typeof param === UNDEF) {
1804						hash = '';
1805						history.pushState('', '', indexName || 'index.html');
1806					} else {
1807						if (hash.charAt(0) === '#') {
1808							hash = hash.substring(1);
1809						}
1810						params = hash.objectify();
1811						if (params.hasOwnProperty(param)) {
1812							delete params[param];
1813							hash = '#' + paramize(params);
1814						}
1815						history.pushState(params, (typeof title === UNDEF)? '' : title, (hash.length > 1)? hash : (indexName || 'index.html'));
1816					}
1817				}
1818			}
1819		},
1820	
1821	// Reading state object
1822	
1823	readParam = function() {
1824		
1825			if (HISTORY) {
1826				if (history.state) {
1827					if (typeof history.state === STRING) {
1828						history.state = history.state.objectify();
1829					}
1830					if (history.state.hasOwnProperty('img')) {
1831						history.state.img = encodeURIComponent(history.state.img);
1832					}
1833					return history.state;
1834				}
1835				
1836				var hash = window.location.hash;
1837				if (hash.charAt(0) === '#') {
1838					hash = hash.substring(1);
1839				}
1840				return hash.objectify();
1841			}
1842			
1843			return null;
1844		},
1845	
1846	// Removing ?search
1847	
1848	removeSearch = function(title) {
1849		
1850			if (HISTORY) {
1851				history.replaceState(history.state, (typeof title === UNDEF)? '' : title, window.location.href.replace(window.location.search, ''));
1852			}
1853		},
1854				
1855	// Restoring scroll position
1856	
1857	restoreScrollPosition = function() {
1858		
1859			if (LOCALSTORAGE) {
1860				var lt = $.cookie('last-touch'),
1861					dt = (new Date()).valueOf(),
1862					p = $.cookie(REL_PATH + PAGE_NAME + ':scroll-pos'),
1863					setTop = function(p, tries) {
1864							if ($('html').height() >= p) {
1865								$('html').scrollTop(p);
1866							} else if (tries) {
1867								setTimeout(setTop, 1000, p, tries - 1);
1868							}
1869						};
1870				
1871				if (lt && ((dt - lt) < 2000) && p) {
1872					// Restore only if within 2 secs of leaving another page in the same album
1873					 setTop(p, 6);
1874				}
1875			}
1876		},
1877		
1878	// Saving scroll position
1879	
1880	saveScrollPosition = function() {
1881			
1882			if (LOCALSTORAGE) {
1883				var p = $('html').scrollTop();
1884				
1885				$.cookie(REL_PATH + PAGE_NAME + ':scroll-pos', p? p : null, 300);
1886				$.cookie('last-touch', (new Date()).valueOf(), 300);
1887			}
1888		},
1889	
1890	// Push all elements of an array into another array
1891	
1892	pushAll = function(a, b) {
1893			if (a instanceof Array) {
1894				if (b instanceof Array) {
1895					for (var i = 0, l = b.length; i < l; i++) {
1896						a.push(b[i]);
1897					}
1898				} else {
1899					a.push(b);
1900				}
1901			}
1902		},
1903		
1904	// Push only elements of an array into another array which are absent
1905	
1906	pushNew = function(a, b) {
1907			if (a instanceof Array) {
1908				if (b instanceof Array) {
1909					for (var i = 0, l = b.length; i < l; i++) {
1910						if (!a.includes(b[i])) {
1911							a.push(b[i]);
1912						}
1913					}
1914				} else {
1915					if (!a.includes(b)) {
1916						a.push(b);
1917					}
1918				}
1919			}
1920		},
1921		
1922	// Video duration in ms
1923	
1924	videoDurationMs = function(d) {
1925			var m = d.match(/(\d{2})\:(\d{2})\:(\d{2})(\.(\d+))?/);
1926			return m? ((m[5]? Math.round(parseFloat('0.' + m[5]) * 1000) : 0) + parseInt(m[3]) * 1000 + parseInt(m[2]) * 60000 + parseInt(m[1]) * 3600000) : null;
1927		},
1928		
1929	// Guess width / height from HTML code
1930	
1931	guessDimensions = function(c) {
1932			var mw,
1933				mh,
1934				d = [];
1935			
1936			if (!c) {
1937				return null;
1938			}
1939			
1940			mw = c.match(/<\w+\s[^>]*\swidth="([\d\.]+)"/i) ||
1941				c.match(/<\w+\s[^>]*\sstyle=".*width:\s?([\d\.]+)(r?em|px)"/i);
1942		
1943			mh = c.match(/<\w+\s[^>]*\sheight="([\d\.]+)"/i) ||
1944				c.match(/<\w+\s[^>]*\sstyle=".*height:\s?([\d\.]+)(r?em|px)"/i);
1945		
1946			if (mw) {
1947				d[0] = parseFloat(mw[1]);
1948				if (mw.length > 2 && mw[2] !== 'px') {
1949					d[0] *= 16;
1950				}
1951				
1952				if (mh) {
1953					d[1] = parseFloat(mh[1]);
1954					if (mh.length > 2 && mh[2] !== 'px') {
1955						d[1] *= 16;
1956					}
1957				} else {
1958					d[1] = .75 * d[0];
1959				}
1960				
1961				return d;
1962			}
1963			
1964			return null;
1965				
1966		},
1967				
1968	// Currency codes
1969	
1970	currencyToHtml = {
1971			'USD': 	'US$',
1972			'EUR': 	'&euro;',
1973			'GBP': 	'GB&pound;',
1974			'JPY': 	'&yen;',
1975			'HUF': 	'Ft',
1976			'CAD':	'CAN$',
1977			'AUD':	'AU$',
1978			'RUB':	'&#8381;'
1979		},
1980		
1981	codeToCurrency = function(code) {
1982			
1983			if (currencyToHtml.hasOwnProperty(code)) {
1984				return currencyToHtml[code];
1985			}
1986			
1987			return code;
1988		},
1989	
1990	currencyToHtmlShort = {
1991			'USD': 	'$',
1992			'EUR': 	'&euro;',
1993			'GBP': 	'&pound;',
1994			'JPY': 	'&yen;',
1995			'HUF': 	'Ft',
1996			'CAD':	'$',
1997			'AUD':	'$',
1998			'RUB':	'&#8381;'
1999		},
2000		
2001	codeToShortCurrency = function(code) {
2002			
2003			if (currencyToHtmlShort.hasOwnProperty(code)) {
2004				return currencyToHtmlShort[code];
2005			}
2006			
2007			return code;
2008		},
2009	
2010	// Printing an image with caption
2011	
2012	printImage = function(src, title, caption) {
2013		
2014			if (!src) {
2015				return;
2016			}
2017			
2018			var pw = window.open('about:blank', 'print', 'location=no,status=no,titlebar=no');
2019			
2020			pw.document.open();
2021			pw.document.write('<!DOCTYPE html>\n<html>\n<head>\n<meta charset="utf-8">\n<title>' + (title || 'Print') + '</title>' +
2022				'\n<script>printOut=function(){window.print();window.close();}</scr' + 'ipt>' + 
2023				'\n<style>body{margin:0;padding:0;text-align:center;overflow:hidden;}\nimg{display:block;width:100%;height:auto;vertical-align:top;}</style>' +
2024				'\n</head>\n<body onLoad="setTimeout(printOut,100)">' +
2025				'<img src="' + src + '">' + (caption || '') + 
2026				'</body>\n</html>');
2027			pw.document.close();
2028		},
2029		
2030	// Getting file size asynchronously and executing the passed function when ready
2031	
2032	getFileSize = function(url, doneFn, args) {
2033			var http = new XMLHttpRequest();
2034				
2035			// Opening HEAD, asynchronous way
2036			http.open('HEAD', url, true); 
2037			
2038			// Handling ready state
2039			http.onreadystatechange = function() {
2040					if (this.readyState == this.DONE) {
2041						if (this.status === 200) {
2042							if (typeof doneFn === FUNCTION) {
2043								if (typeof args !== UNDEF) {
2044									doneFn.call(this, [ this.getResponseHeader('content-length') ], args);
2045								} else {
2046									doneFn.call(this, [ this.getResponseHeader('content-length') ]);
2047								}
2048							}
2049						}
2050					}
2051				};
2052				
2053			// Submitting request
2054			http.send(); 
2055		},
2056	
2057	// Scroll body or html element so el (the target element) get in view
2058	
2059	moveInView = function(el) {
2060			var te = $(el);
2061			
2062			if (te.length && te.is(':visible')) {
2063				var to = te.offset().top,
2064					target = navigator.userAgent.match(/(iPod|iPhone|iPad)/)? $('body') : $('html'),
2065					st = target.scrollTop();
2066				
2067				if (to < st || to > (st + window.outerHeight - 80)) {
2068					if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
2069						window.scrollTo(0, to);
2070					} else {
2071						target.animate({ 
2072								scrollTop: 		 to
2073							}, 500, function() {
2074								target.clearQueue();
2075							});
2076					}
2077				}
2078			}
2079		},
2080		
2081	// Simple decryption
2082
2083	xDecrypt = function(c) {
2084		
2085			if (typeof c !== 'string') {
2086				return '';
2087			}
2088			
2089			var xs = [0x93,0xA3,0x57,0xFE,0x99,0x04,0xC6,0x17],
2090				cl = c.length,
2091				sl = Math.ceil(cl / 8) * 5,
2092				src = new Array(sl), 
2093				r = '', 
2094				i, 
2095				j = 0, 
2096				k, 
2097				v;
2098				
2099			for (i = 0; i < sl; i++) {
2100				src[i] = 0;
2101			}
2102			
2103			for (i = 0; i < cl; i++) {
2104				if ((v = c.charCodeAt(i) - 0x30) > 9) {
2105					v -= 7;
2106				}
2107				v <<= 11 - j % 8;
2108				k = j >> 3;
2109				if (k < sl) {
2110					src[k] |= v >> 8;
2111					if (++k < sl) {
2112						src[k] |= v & 0xff;
2113					}
2114				}
2115				j += 5;
2116			}
2117			
2118			for (i = 0; i < sl; i++) {
2119				src[i] ^= xs[i % 8];
2120			}
2121			
2122			sl = src[0] | (src[1] << 8);
2123			
2124			for (v = 0, i = 4; i < sl; i++) {
2125				r += String.fromCharCode(src[i]);
2126				v += src[i];
2127			}
2128			
2129			if (v != ((src[2] & 0xff) | (src[3] << 8)))
2130				r = '';
2131			
2132			return r;
2133		},
2134		
2135	// Getting computed style
2136	
2137	getStyle = function(el, style) {
2138		
2139			if (el instanceof Element) {
2140				if (document.defaultView && document.defaultView.getComputedStyle) {
2141					return document.defaultView.getComputedStyle(el, '').getPropertyValue(style.unCamelCase());
2142				} else if (el.currentStyle) {
2143					return el.currentStyle[style];
2144				}
2145			}
2146			
2147			return null;
2148		},
2149		
2150	// Testing scrollbar width for mobile detection
2151	
2152	scrollbarWidth = function() {
2153		
2154			var div = document.createElement("div"),
2155				sw = 0;
2156				
2157			div.style.cssText = 'width:100px;height:100px;overflow:scroll !important;position:absolute;top:-9999px';
2158			if (document.body) {
2159				document.body.appendChild(div);
2160				sw = div.offsetWidth - div.clientWidth;
2161				document.body.removeChild(div);
2162			}
2163			
2164			return sw;
2165		},
2166		
2167	// Adding class without jQuery (can be used anytime)
2168	
2169	addClass = function(el, className) {
2170		
2171			if (el.classList) {
2172				el.classList.add(className);
2173			} else {
2174				el.className += ' ' + className;
2175			}
2176		},
2177		
2178	// Is the page zoomed?
2179	
2180	pageZoomed = function() {
2181		
2182			if (window.hasOwnProperty('visualViewport')) {
2183				return window.visualViewport.scale > ZOOM;
2184			}
2185			
2186			return false;
2187		},
2188		
2189	/*********************************************************
2190	 *					Video play
2191	 */
2192	 
2193	playVideo = function(el, doneFn) {
2194			
2195			if (el && el.nodeName === 'VIDEO') {
2196				
2197				if (el.paused) {
2198					let playPromise = el.play();
2199					
2200					if (typeof playPromise !== UNDEF) {
2201						playPromise.then(() => {
2202								// Plays immediately
2203								addClass(el, 'playing');
2204								if (typeof doneFn === FUNCTION) {
2205									doneFn.call(null);
2206								}
2207							}).catch(err => {
2208								if (err.name === 'NotAllowedError') {
2209									// Trying muted
2210									el.muted = true;
2211									el.play().then(() => {
2212											// Works
2213											console.log('Falling back to muted autoplay! Unmute the video manually!');
2214											addClass(el, 'playing');
2215											if (typeof doneFn === FUNCTION) {
2216												doneFn.call(null);
2217											}
2218										}).catch(err => {
2219											// Rejected
2220											console.log('Autoplay rejected. Try starting the video manually!');
2221										});
2222								} else {
2223									// Other error
2224									console.log('Video format not supported!');
2225								}
2226							});
2227					}
2228				} else {
2229					if (typeof doneFn === FUNCTION) {
2230						doneFn.call(null);
2231					}
2232				}
2233			}	
2234		},
2235	 
2236	/*********************************************************
2237	 *					Full Screen API
2238	 */
2239	 
2240	// Saving the last full screen change date
2241	
2242	fullscreenChanged = 0,
2243
2244	// Querying the last full screen change in MS from now
2245	
2246	fullscreenChangedSince = function() {
2247			return new Date() - fullscreenChanged;
2248		},
2249		
2250	// Fill screen API
2251	
2252	hasFullscreen = function() {
2253			// Has Fullscreen API? 
2254			return 		document.fullscreenEnabled || 
2255						document.mozFullScreenEnabled ||
2256						document.webkitFullscreenEnabled || 
2257						document.msFullscreenEnabled;
2258		},
2259	
2260	isFullscreen = function() {
2261			// Checking state
2262			return !!(	document.fullscreenElement || 
2263						document.mozFullScreenElement ||
2264						document.webkitFullscreenElement || 
2265						document.msFullscreenElement);
2266		},
2267		
2268	// Listening to externally triggered full screen exit
2269	
2270	onFullscreenChange = function(fn) {
2271		
2272			let handleFullscreenChange = function(e) {
2273						fn(isFullscreen());
2274					};
2275					
2276			if (typeof fn === FUNCTION) {
2277				["fullscreenchange","webkitfullscreenchange","mozfullscreenchange","msfullscreenchange"].forEach(et => document.addEventListener(et, handleFullscreenChange, false));
2278			}
2279		},
2280		
2281	// Requesting full screen
2282	
2283	requestFullscreen = function(success, fail) {
2284			
2285			if (hasFullscreen()) {
2286				if (isFullscreen()) {
2287					// Already in fullscreen
2288					if (typeof success === FUNCTION) {
2289						success.call();
2290					}
2291					
2292				} else {
2293					// Not yet
2294					let	de = document.documentElement;
2295					
2296					if (de.requestFullscreen) {
2297						// Standard: using promise
2298						de.requestFullscreen().then(success || (() => {})).catch(err => {
2299							if (typeof fail === FUNCTION) {
2300								fail.call();
2301							} else {
2302								console.log('Full-screen error: ' + err.message + ' (' + err.name + ')');
2303							}
2304						});
2305
2306					} else if (de.mozRequestFullscreen) {
2307						// Older Mozilla <= 63
2308						de.mozRequestFullscreen();
2309						success.call();
2310					} else if (de.webkitRequestFullscreen) {
2311						// Safari does not return a promise
2312						de.webkitRequestFullscreen();
2313						success.call();
2314					} else if (de.msRequestFullscreen) {
2315						// IE11
2316						de.msRequestFullscreen();
2317						success.call();
2318					}
2319					fullscreenChanged = new Date();
2320				}
2321			}
2322		},
2323		
2324	// Requesting exit full screen
2325	
2326	exitFullscreen = function(success, fail) {
2327			
2328			if (hasFullscreen()) {
2329				if (!isFullscreen()) {
2330					// Not in fullscreen
2331					if (typeof success === FUNCTION) {
2332						success.call();
2333					}
2334					
2335				} else {
2336					// In fullscreen -> Exit
2337					if (typeof success !== FUNCTION) {
2338						success = function() {};
2339					}
2340					
2341					if (typeof fail !== FUNCTION) {
2342						fail = function(err) { console.log('Full-screen error: ' + err.message + ' (' + err.name + ')'); };
2343					}
2344							
2345					if (document.exitFullscreen) {
2346						document.exitFullscreen().then(success).catch(fail);
2347					} else if (document.mozExitFullscreen) {
2348						document.mozExitFullscreen().then(success).catch(fail);
2349					} else if (document.webkitExitFullscreen) {
2350						// Safari fails with promises
2351						document.webkitExitFullscreen();
2352						success.call();
2353					} else if (document.msExitFullscreen) {
2354						document.msExitFullscreen().then(success).catch(fail);
2355					}
2356					fullscreenChanged = new Date();
2357				}
2358			}
2359		},
2360		
2361	// Orientation change listener
2362	
2363	orientationChanged = function(e, success) {
2364			if (e.type === 'change') {
2365				if (e.target.type.startsWith('landscape') && !isFullscreen()) {
2366					requestFullscreen(success);
2367				}
2368			} else if (e.type === 'orientationchange') {
2369				if (e.target.innerWidth >
2369 e.target.innerHeight && !isFullscreen()) {
2370					requestFullscreen(success);
2371				}
2372			}
2373		},
2374	
2375	goFullscreenOnLandscapeChange = function() {
2376			
2377			if (document.fullscreenEnabled || 
2378				document.mozFullScreenEnabled ||
2379				document.webkitFullscreenEnabled || 
2380				document.msFullscreenEnabled
2381			) {
2382				if (typeof screen !== UNDEF && screen.hasOwnProperty('orientation')) {
2383					// Has Screen API
2384					screen.orientation.addEventListener('change', orientationChanged);
2385				} else {
2386					// deprecated orientationchange event
2387					window.addEventListener('orientationchange', orientationChanged);
2388				}
2389			}
2390		},
2391		
2392	/*********************************************************
2393	 *					Wake lock API
2394	 */
2395	
2396	// Saving wakelock promise
2397	
2398	wakeLock = null,
2399	
2400	// Requesting wake lock
2401	
2402	requestWakeLock = async (success, fail) => {
2403					
2404			if ('wakeLock' in navigator && 'request' in navigator.wakeLock) {
2405				
2406				wakeLock = null;
2407		
2408				try {
2409					
2410					wakeLock = await navigator.wakeLock.request('screen');
2411					/*
2412					if (DEBUG) {
2413						console.log('Wakelock activated!');
2414					}
2415					*/
2416					if (typeof success === FUNCTION) {
2417						success.call();
2418					}
2419					
2420				} catch (e) {
2421					/*
2422					if (DEBUG) {
2423						console.log('Wakelock cannot be activated!');
2424					}
2425					*/
2426					if (typeof fail === FUNCTION) {
2427						fail.call(e);
2428					}
2429				} 
2430			}
2431		},
2432		
2433	// Releasing wake lock
2434	
2435	releaseWakeLock = (success, fail) => {
2436		
2437			if (wakeLock) {
2438				
2439				wakeLock.release().then(() => {
2440						
2441						wakeLock = null;
2442						/*
2443						if (DEBUG) {
2444							console.log('Wakelock released!');
2445						}
2446						*/
2447						if (typeof success === FUNCTION) {
2448							success.call();
2449						}
2450					});
2451			}
2452		},
2453	
2454
2455	/*
2456	 *	loadExternal: Trying to load data1.json of an external album
2457	 */
2458	
2459	loadExternal = function(url, doneFn) {
2460		
2461			if (url.endsWith(PAGE_NAME)) {
2462				url = url.slice(0, - PAGE_NAME.length);
2463			}
2464			
2465			if (!url.endsWith('/')) {
2466				url += '/';
2467			}
2468				
2469			// Loading data1.json
2470			return $.getJSON(url + 'data1.json')
2471				.done(function(d) {
2472						if (typeof doneFn === FUNCTION) {
2473							doneFn.call(d);
2474						}
2475					})
2476				.fail(function(jqxhr, status, error) {
2477						if (DEBUG) {
2478							console.log('Not an album, or cannot load album database for: "' + url + '".');
2479						}
2480					});
2481		},
2482			
2483	/*********************************************************
2484	 *					Loading external API's
2485	 */
2486	
2487	// Loading Google Analytics
2488	
2489	loadGoogleAnalytics = function(siteID, type, supportDoubleclick) {
2490			
2491			if (LOCAL) {
2492				return;
2493			}
2494			var sid = xDecrypt(siteID);
2495			if (type === 'universal') {
2496				// analytics.js
2497				(function(i,s,o,g,r,a,m){
2498						i['GoogleAnalyticsObject']=r;
2499						i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();
2500						a=s.createElement(o),m=s.getElementsByTagName(o)[0];
2501						a.async=1;
2502						a.src=g;
2503						m.parentNode.insertBefore(a,m)
2504					})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
2505				ga('create', sid, 'auto');
2506				if (supportDoubleclick) {
2507					ga('require','displayfeatures');
2508				}
2509				ga('send', 'pageview');
2510			} else {
2511				window.dataLayer = window.dataLayer || [];
2512				window.gtag = function() {dataLayer.push(arguments);}
2513				window.gtag('js', new Date());
2514				if (DEBUG) {
2515					window.gtag('config', sid, { 'debug_mode': true });
2516					console.log('gtag:', typeof gtag);
2517					console.log('dataLayer:', window.dataLayer);
2518				} else {
2519					window.gtag('config', sid);
2520				}
2521				//gtag('config',sid,{'send_page_view':true});
2522			}
2523		},
2524		
2525	// Loading Google API
2526	
2527	loadGoogleAPI = function(badgeId, boxType) {
2528			if (LOCAL) {
2529				return;
2530			}
2531			(function(i,s,o,g,r,a,m){
2532				a=s.createElement(o);
2533				a.appendChild(s.createTextNode(r));
2534				m=s.getElementsByTagName(o)[0];
2535				a.async=1;
2536				a.src=g;
2537				m.parentNode.insertBefore(a,m);
2538			})(window,document,'script',('https:'==document.location.protocol?'https:':'http:')+'//apis.google.com/js/platform.js',"{parsetags:'explicit',lang:'${lang}'}");
2539			
2540			var la = 30,
2541				// Multiple try if ready
2542				launch = function() {
2543						if (typeof gapi === UNDEF) {
2544							if (la--) {
2545								setTimeout(launch, 200);
2546							} else if ('console' in window) {
2547								console.log('Google Plus API failed to load!');
2548							}
2549						} else {
2550							setTimeout(function() {
2551								gapi[boxType].go();
2552							}, 200);
2553						}
2554					};
2555					
2556			if (badgeId) {
2557				launch();
2558			}
2559		},
2560		
2561	// Loading Facebook API
2562	
2563	loadFacebookAPI = function(appId,locale) {
2564			if (LOCAL) {
2565				return;
2566			}
2567			window.fbAsyncInit = function(){
2568				if (typeof jQuery !== UNDEF) {
2569					jQuery('.social-links').trigger('updateLinks');
2570				}
2571			};
2572			(function(d,s,id){
2573				var js, 
2574				fjs = d.getElementsByTagName(s)[0];
2575				if (d.getElementById(id)) return;
2576				js = d.createElement(s); 
2577				js.id = id;
2578				js.src = 'https://connect.facebook.net/'+(locale||'en_US')+'/sdk.js#xfbml=1&version=v3.0&appId='+xDecrypt(appId);
2579				fjs.parentNode.insertBefore(js, fjs);
2580			})(document, 'script', 'facebook-jssdk');
2581		},
2582		
2583	// Loading Disqus API
2584	
2585	loadDisqusAPI = function(appId) {
2586			if (LOCAL) {
2587				return;
2588			}
2589			(function(d,s) {
2590				s = d.createElement(s);
2591				s.async = 1;
2592				s.src='https://'+xDecrypt(appId)+'.disqus.com/embed.js';
2593				s.setAttribute('data-timestamp', +new Date());
2594				(d.head || d.body).appendChild(s);
2595			})(document,'script');
2596		},
2597		
2598	// Loading Pinterest API
2599	
2600	loadPinterestAPI = function() {
2601			if (LOCAL) {
2602				return;
2603			}
2604			(function(d,s) {
2605				s = d.createElement(s);
2606				s.async = 1;
2607				s.src = 'https://assets.pinterest.com/js/pinit.js';
2608				(d.head || d.body).appendChild(s);
2609			})(document,'script');
2610		},
2611	
2612	// Loading all APIs
2613	
2614	loadAPIs = function(apis, allowed) {
2615		
2616			if (apis && !isEmpty(apis)) {
2617				
2618				for (var a in apis) {
2619					
2620					if (typeof allowed === UNDEF || allowed[a]) {
2621						
2622						switch (a) {
2623						
2624							case 'googleAnalytics':
2625								loadGoogleAnalytics(apis[a][0],apis[a][1],apis[a][2]);
2626								break;
2627								
2628							case 'googlePlus':
2629								loadGoogleAPI(apis[a][0],apis[a][1]);
2630								break;
2631								
2632							case 'facebook':
2633								loadFacebookAPI(apis[a][0]);
2634								break;
2635								
2636							case 'disqus':
2637								loadDisqusAPI(apis[a][0]);
2638								break;
2639								
2640							case 'pinterest':
2641								loadPinterestAPI();
2642								break;
2643						}
2644					}
2645				}
2646			}
2647		},
2648	
2649	// Asking permission first to load APIs
2650	
2651	askTrackingConsent = function(settings, apis) {
2652		
2653			if ((!apis || isEmpty(apis)) && !settings['cookiePolicy']) {
2654				// No API's and no Cookie Policy
2655				return;
2656			}
2657			
2658			if (LOCAL) {
2659				// Don't ask for API's locally
2660				return;
2661			}
2662			
2663			settings = $.extend({}, {
2664					stay: 			15,
2665					buttonClass:	'button'
2666				}, settings);
2667			
2668			apis = apis || {};
2669			
2670			var text = getTranslations({		// Translated texts
2671						close:							"Close",
2672						cookiePolicyText:				"This album is using cookies to remember your preferences. It does not track you, nor does it store personal data.",
2673						cookiePolicyAgree:				"Got it",
2674						gdprComplianceText:				"This site uses the following external services that may track your visits anonymously. Disabling tracking will stop their functionality.",
2675						allowAll:						"Allow all",
2676						denyAll:						"Deny all",
2677						allowSelected:					"Allow selected",
2678						cookiePolicyLearnMore:			"Learn more"
2679					}),
2680				consent = $.albumCookie('trackingConsent'),
2681				showApis = false,
2682				showCp = false,
2683				oncl = 'icon-checkmark',
2684				
2685				// Toggle one button
2686				toggleApi = function(e) {
2687						var b = $(e.target),
2688							sel = b.hasClass(oncl);
2689					
2690						b.toggleClass(oncl, !sel);
2691						
2692						setButtons();
2693					},
2694				
2695				// Set visibility of "Allow all", "Deny all" and "Allow selected" buttons
2696				setButtons = function() {
2697						var any = $('#trackingConsent span.' + oncl).length > 0;
2698						
2699						$('#allow,#deny').toggle(!any);
2700						$('#allowsel').toggle(any);
2701					},
2702					
2703				// Saving the cookie, and launching the APIs
2704				saveCookie = function(e) {
2705						var allowed,
2706							btns = $('#trackingConsent span');
2707						
2708						// Based on the old setting
2709						allowed = $.extend({}, $.albumCookie('trackingConsent'), { cookie: 1 });
2710						
2711						if (this.id === 'allow') {
2712							btns.each(function() {
2713								$(this).addClass(oncl);
2714								allowed[$(this).attr('id')] = 1;
2715							});
2716						} else if (this.id === 'deny') {
2717							btns.each(function() {
2718								$(this).removeClass(oncl);
2719								allowed[$(this).attr('id')] = 0;
2720							});
2721						} else if (this.id === 'allowsel') {
2722							btns.each(function(i) {
2723								allowed[$(this).attr('id')] = $(this).hasClass(oncl);
2724							});
2725						}
2726						
2727						$.albumCookie('trackingConsent', allowed, 31622400); // lasts 1 year
2728						
2729						removePopup();
2730						
2731						loadAPIs(apis, allowed);
2732						
2733						return false;
2734					},
2735					
2736				// Check for missing API consents
2737				matchApis = function() {
2738						
2739						for (var a in apis) {
2740							if (!consent.hasOwnProperty(a)) {
2741								return false;
2742							}
2743						}
2744						return true;
2745					},
2746				
2747				// Removing popup
2748				removePopup = function() {
2749						$('#cookiepolicy').on('transitionend', function() {
2750								$(this).remove();
2751							}).css({
2752								opacity:	0,
2753								transform:	'translate(-50%,120%)'
2754							});
2755					};
2756			
2757			if (!consent) {
2758				// No consent asked before
2759				showApis = !isEmpty(apis);
2760				showCp = settings.hasOwnProperty('cookiePolicy') && settings.cookiePolicy;
2761			} else {
2762				// Consent found: checking for changes
2763				showApis = !matchApis(consent, apis);
2764				showCp = settings.hasOwnProperty('cookiePolicy') && settings.cookiePolicy && !consent.hasOwnProperty('cookie');
2765			}
2766		
2767			if (!showApis && !showCp) {
2768				// Already allowed: load immediately
2769				loadAPIs(apis, consent);
2770				
2771			} else {
2772				// Ask consent
2773				
2774				// Container
2775				var	panel = $('<div>', { 
2776							id: 	'cookiepolicy' 
2777						}).appendTo($('body')),
2778					texts = $('<div>', { 
2779							'class': 	'texts' 
2780						}).appendTo(panel),
2781					btns = $('<div>', { 
2782							'class': 	'buttons' 
2783						}).appendTo(panel),
2784					cpt;
2785					
2786				// Close button
2787				panel.append($('<a>', {
2788						'class':	'close icon-close ' + settings.buttonClass,
2789						title:		text.close
2790					}).on('click', removePopup));
2791				
2792				if (showCp) {
2793					// Showing cookie policy text 
2794					cpt = $('<p>', {
2795							id:			'cookiePolicy',
2796							html: 		text.cookiePolicyText 
2797						}).appendTo(texts);
2798					
2799					if (!showApis) {
2800						// Only Cookie policy: add Agree button + readmore
2801						btns.append($('<a>', {
2802								id:			'agree',
2803								'class': 	'icon-ok ' + settings.buttonClass,
2804								text: 		' ' + text.cookiePolicyAgree 
2805							}).on('click', saveCookie));
2806						
2807					}
2808				}
2809				
2810				if (showApis) {
2811					// Show APIs
2812					cpt = $('<p>', { 
2813							id:			'trackingConsent',
2814							html: 		text.gdprComplianceText 
2815						}).appendTo(texts);
2816							
2817					for (var a in apis) {
2818						cpt.append($('<span>', {
2819								id:			a,
2820								'class':	'checkbox' + ((consent && consent[a])? (' ' + oncl) : ''),
2821								html:		'&nbsp;' + a.capitalize()
2822							})
2823							.on('click', toggleApi));
2824					}
2825					
2826					btns.append($('<a>', {
2827							id:			'allow',
2828							'class': 	'icon-ok ' + settings.buttonClass,
2829							text: 		' ' + text.allowAll 
2830						}).on('click', saveCookie));
2831					
2832					btns.append($('<a>', {
2833							id:			'deny',
2834							'class': 	'alert icon-close ' + settings.buttonClass,
2835							text: 		' ' + text.denyAll 
2836						}).on('click', saveCookie));
2837					
2838					btns.append($('<a>', {
2839							id:			'allowsel',
2840							'class': 	'icon-ok ' + settings.buttonClass,
2841							text: 		' ' + text.allowSelected 
2842						}).on('click', saveCookie));
2843				
2844					setButtons();
2845				}
2846				
2847				if (settings.cookiePolicyUrl) {
2848					texts.append(' ').append($('<a>', { 
2849							text: 		text.cookiePolicyLearnMore, 
2850							target: 	'_blank', 
2851							href: 		settings.cookiePolicyUrl 
2852						}));
2853				}
2854				
2855				panel.css({
2856						opacity:	1,
2857						transform:	'translate(-50%,0)'
2858					});
2859						
2860				setTimeout(removePopup, settings.stay * 1000);
2861			
2862			}
2863		};
2864
2865
2866	// Adding 'touch' : 'no-touch' classes
2867	addClass(document.getElementsByTagName('html')[0], (TOUCHENABLED? '' : 'no-') + 'touch');
2868	// Adding 'hidpi' : 'no-hdpi' classes
2869	addClass(document.getElementsByTagName('html')[0], (HIDPI? '' : 'no-') + 'hidpi');
2870
2871/*	
2872 *	Debugging functions
2873 */
2874
2875// Waiting for jQuery loaded
2876
2877(function($, undefined) {
2878	'use strict';
2879	// log: logging function
2880	
2881	var _logel, 
2882		_logover = false, 
2883		_lastlog, 
2884		_lastcnt = 1;
2885	
2886	log = function(c) {
2887		
2888			var resolveObject = function(c) {
2889				if (Array.isArray(c)) {
2890					var s = '';
2891					for (var i = 0; i < c.length; i++) {
2892						s += resolveObject(c[i]) + ', ';
2893					}
2894					return '[ ' + s.substring(0, s.length-2) + ' ]';
2895				} else if (typeof c === 'object') {
2896					var s = '';
2897					for (var i in c) {
2898						s += i + ': ' + resolveObject(c[i]) + ',<br>';
2899					}
2900					return '{ ' + s + ' }';
2901				} else if (isNaN(c)) {
2902					return c;
2903				} else {
2904					return (parseInt(c) === c)? c : c.toFixed(4);
2905				}
2906			};
2907						
2908			if (!DEBUG || _logover) {
2909				return;
2910			}
2911			if (!_logel) {
2912				_logel = $('<div id="log" style="position:fixed;left:0;top:0;width:200px;bottom:0;overflow:auto;padding:10px;background-color:rgba(0,0,0,0.5);color:#fff;font-size:0.75em;z-index:999999"></div>').hover(function(){
2913					_logover = true;
2914				}, function() {
2915					_logover = false;
2916				}).appendTo('body');
2917			}
2918			if (c === _lastlog) {
2919				_logel.children().first().empty().html(_lastlog + ' <sup>(' + (++_lastcnt) + ')</sup>');
2920			} else {
2921				$('<div style="height:3em;overflow:auto;">' + resolveObject(c) + '</div>').prependTo(_logel);
2922				_lastlog = c;
2923				_lastcnt = 1;
2924			}
2925		};
2926	
2927	// logEvents :: debugging events
2928	
2929	$.fn.logEvents = function( e ) {
2930			
2931			if (!DEBUG) {
2932				return;
2933			}
2934			
2935			var events = e || 'mousedown mouseup mouseover mouseout mousewheel wheel dragstart click blur focus load unload reset submit change abort cut copy paste selection drag drop orientationchange touchstart touchmove touchend touchcancel pointerdown pointermove pointerup MSPointerDown MSPointerMove MSPointerUp gesturestart gesturechange gestureend';
2936	
2937			return this.each(function() {
2938				$(this).on(events, function(e) {
2939					if (typeof e === UNDEF) {
2940						log('Undefined event');
2941					} else if (e.target) {
2942						if (e.target.id !== 'log') { 
2943							log(e.type + ' <span style="padding:0 4px;font-size:0.75em;background-color:#000;border-radius:4px;"><b>' + (e.target.nodeName? e.target.nodeName.toLowerCase() : '???') + '</b>' + (e.target.id? (':'+e.target.id) : '') + '</span>' + 
2944								(e.relatedTarget? (' <span style="padding:0 4px;font-size:0.6em;background-color:#800;border-radius:4px;"><b>' + e.relatedTarget.nodeName.toLowerCase() + '</b>' + (e.relatedTarget.id? (':'+e.relatedTarget.id) : '') + '</span>') : ''));
2945						}
2946					} else {
2947						log('No event target!');
2948					}
2949					return true;
2950				});
2951			});
2952		};
2953	
2954	// logCss :: tracks css values until the element is live
2955	
2956	$.fn.logCss = function( p, dur, step ) {
2957		
2958			if (!DEBUG) {
2959				return;
2960			}
2961			
2962			step = step || 20;
2963			dur = dur || 2000;
2964			var t0 = new Date();
2965			
2966			return this.each(function() {
2967				var el = $(this);
2968				var show = function( nm ) {
2969					var t = new Date() - t0;
2970					log(t + '&nbsp;::&nbsp;' + nm + ' = ' + el.css(nm));
2971					if (t > dur) {
2972						clearInterval(iv);
2973					}
2974				};
2975				
2976				var iv = setInterval(function() {
2977					if ( Array.isArray(p) ) {
2978						for (var i = 0; i < p.length; i++) {
2979							show(p[i]);
2980						}
2981					}
2982					else {
2983						show(p);
2984					}
2985				}, step);
2986			});
2987		},
2988		
2989	// Focus an element
2990	
2991	$.fn.setFocus = function(options) {
2992			
2993			if (this.length) {
2994				if (typeof options !== UNDEF) {
2995					this[0].focus(options);
2996				} else {
2997					this[0].focus();
2998				}
2999			}
3000		};
3001		
3002	// Deserialize a serialized string
3003	
3004	if ($.fn.deserialize === undefined) {
3005		$.fn.deserialize = function (s) {
3006				
3007				if (!s) {
3008					return this;
3009				}
3010			
3011				var self = $(this),
3012					values = s.replace(/\+/g, '%20').split('&');
3013			
3014				$.each(values, function(i, pair) {
3015					var val = pair.split('='),
3016						name = decodeURIComponent(val[0]),
3017						value = (val[1] !== null)? decodeURIComponent(val[1]) : '',
3018						field = self.find('[name=' + name + ']');
3019					
3020					if (!field.length) {
3021						return true;
3022					}
3023					
3024					if (field[0].type === 'radio' || field[0].type === 'checkbox') {
3025						
3026						var fieldVal = field.filter('[value="' + value + '"]'),
3027							found = fieldVal.length;
3028							
3029						if (!found && value === 'on') {
3030							field.eq(0).prop('checked', true);
3031						} else {
3032							fieldVal.prop('checked', found);
3033						}
3034						
3035					} else { 
3036						// input, textarea
3037						field.val(value);
3038					}
3039				});
3040				
3041				return this;
3042			};
3043	}
3044	
3045	// Making $.when working with arrays
3046	
3047	if ($.when.all === undefined) {
3048		$.when.all = function(deferreds) {
3049				var deferred = new $.Deferred();
3050				$.when.apply($, deferreds).then(
3051					function() {
3052						deferred.resolve(Array.prototype.slice.call(arguments));
3053					},
3054					function() {
3055						deferred.reject(Array.prototype.slice.call(arguments));
3056					}
3057				);
3058				return deferred;
3059			};
3060	}
3061	
3062	// Get rotation angle in degrees
3063	
3064	$.fn.getRotate = function() {
3065		
3066			if (this && this.length) {
3067				var st = window.getComputedStyle(this[0], null),
3068					mx = st.getPropertyValue('transform') || st.getPropertyValue('-ms-transform') || st.getPropertyValue('-webkit-transform'),
3069					a;
3070					
3071				if (mx && (a = mx.match(/matrix3d\(([^\)]+)\)/)) && a && a.length > 2) {
3072					a = a[1].split(/\s*,/);
3073					return Math.round(Math.atan2(parseFloat(a[1]), parseFloat(a[0])) * (180 / Math.PI));
3074				}
3075			}
3076			return 0;
3077		};
3078	
3079	// Get 3D or 2D matrix as Array of float numbers
3080	
3081	$.fn.getMatrix = function() {
3082		
3083			if (this && this.length) {
3084				var st = window.getComputedStyle(this[0], null),
3085					mx = st.getPropertyValue('transform') || st.getPropertyValue('-ms-transform') || st.getPropertyValue('-webkit-transform'),
3086					v,
3087					m = [];
3088					
3089				if (mx && (v = mx.match(/matrix(3d)?\(([^\)]+)\)/)) && v && v.length > 2) {
3090					v = v[2].split(/,\s*/);
3091					v.forEach(function(a) { m.push(parseFloat(a)); });
3092					return m;
3093				}
3094			}
3095			return [ 1, 0, 0, 1, 0, 0 ];
3096		};
3097		
3098	// Get XY translate coords
3099	
3100	$.fn.getTranslate = function() {
3101		
3102			if (this && this.length) {
3103				var mx = this.getMatrix();
3104				return [ mx[4], mx[5] ];
3105			}
3106			return [ 0, 0 ];
3107		};
3108		
3109	// Get scale
3110	
3111	$.fn.getScale = function() {
3112		
3113			if (this && this.length) {
3114				var mx = this.getMatrix();
3115				return [ mx[0], mx[3] ];
3116			}
3117			return [ 1, 1 ];
3118		};
3119		
3120	// Reverse order
3121	
3122	$.fn.backwards = [].reverse;
3123		
3124	// Waiting for all images
3125	
3126	$.fn.waitAllImg = function(doneFn, successFn, failFn) {
3127		
3128			if (!this || !this.length) {
3129				doneFn.call(this);
3130				return;
3131			}
3132			
3133			var self = $(this),
3134				deferreds = [],
3135				
3136				loadImage = function(image) {
3137					var deferred = new $.Deferred(),
3138						el = new Image();
3139						
3140					el.onload = function() {
3141							deferred.resolve(image);
3142						};
3143					
3144					el.onerror = function() {
3145							deferred.reject(new Error('Image not found: ' + image.src));
3146						};
3147					
3148					el.src = image.src;
3149					
3150					return deferred;
3151				},
3152				
3153				loadVideo = function(video) {
3154					var deferred = new $.Deferred(),
3155						el = document.createElement("VIDEO");
3156						
3157					el.addEventListener('loadedmetadata', function() {
3158							deferred.resolve(video);
3159						});
3160					
3161					el.addEventListener('error', function() {
3162							deferred.reject(new Error('Video not found: ' + video.src));
3163						});
3164					
3165					el.src = video.src;
3166					
3167					return deferred;
3168				},
3169				
3170				loadImages = function(items) {
3171					
3172					items.filter('img[src]').not('[src=""]').each(function() {
3173							deferreds.push(loadImage(this));
3174						});
3175					
3176					items.filter('video[src]').not('[src=""]').each(function() {
3177							deferreds.push(loadVideo(this));
3178						});
3179					
3180					return $.when.all(deferreds);
3181				};
3182			
3183			loadImages(self).then(
3184				
3185				function(self) {
3186					if (typeof successFn === FUNCTION && successFn !== doneFn) {
3187						successFn.call(self);
3188					}
3189				},
3190				
3191				function(err) {
3192					if (typeof failFn === FUNCTION) {
3193						failFn.call(err);
3194					}
3195				}
3196				
3197			).then(function() {
3198				if (typeof doneFn === FUNCTION) {
3199					doneFn.call(self);
3200				}
3201			});
3202			
3203			return this;
3204		};
3205	
3206	// Asking permission before following a link
3207	
3208	$.fn.askPermission = function(data) {
3209			/*
3210				text.restrictedLinkTitle
3211				text.restrictedLinkQuestion
3212				text.restrictedLinkYes
3213				text.restrictedLinkNo
3214			*/
3215			var text = getTranslations({		// Translated texts
3216						restrictedLinkTitle:		'Restricted material',
3217						restrictedLinkQuestion:		'Please verify your age to proceed!',
3218						restrictedLinkYes:			'I´m over 18',
3219						restrictedLinkNo:			'I´m below 18'
3220					}),
3221			
3222				ns = '_lap_',
3223			
3224				getOptions = function(el) {
3225						var opt = {},
3226							o = el.data('ask-permission');
3227							
3228						if (o) {
3229							o = o.split('::');
3230							if (o[0] !== null) {
3231								opt.restrictedLinkTitle = o[0];
3232								if (o[1] !== null) {
3233									opt.restrictedLinkQuestion = o[1];
3234									if (o[2] !== null) {
3235										opt.restrictedLinkYes = o[2];
3236										if (o[3] !== null) {
3237											opt.restrictedLinkNo = o[3];
3238										}
3239									}
3240								}
3241							}
3242						}
3243						
3244						return opt;
3245					};
3246						
3247			return $(this).filter('[href]').each(function() {
3248					
3249					$(this).data('href', $(this).attr('href'));
3250					$(this).attr('href', null);
3251					
3252					$(this).on('click.' + ns, function(e) {
3253							var opt = $.extend({}, text, getOptions($(this)));
3254							
3255							e.preventDefault();
3256							opt.link = $(this).data('href');
3257							
3258							$('body').modal($('<div class="text-center">' + opt.restrictedLinkQuestion + '</div>'), 
3259									[
3260										{	
3261											t:		opt.restrictedLinkYes,
3262											c:		'icon-checkmark',
3263											h:		function() {
3264															window.location.href = opt.link;
3265														}
3266										},
3267										{
3268											t:		opt.restrictedLinkNo,
3269											c:		'icon-close alert',
3270											h:		function() {
3271															// do nothing: close dialog
3272															return true;
3273														}
3274										}
3275									],
3276									{
3277										icon:		'warning',
3278										'class':	'small warning',
3279										title:		opt.restrictedLinkTitle
3280									}
3281								);
3282							
3283							return false;
3284					});
3285				});
3286		};
3287		
3288})(jQuery);
3289;
3290/* 
3291 *	The Album model
3292 * 
3293 *	(c) Laszlo Molnar, 2015, 2017, 2020, 2021
3294 *	Licensed under Creative Commons Attribution-NonCommercial-ShareAlike 
3295 *	<http://creativecommons.org/licenses/by-nc-sa/3.0/>
3296 *
3297 *	requires: jQuery, laza.util
3298 */
3299
3300 
3301/*
3302 * 	Constants
3303 */
3304 
3305const J = {
3306			// jAlbum variables
3307			ALBUM:					'album',				// jAlbum's album data 
3308			FOLDERS: 				'folders',				// Folders array
3309			NAME: 					'name',					// FileName of the item
3310			PATH: 					'path',					// Path to file
3311			THUMB: 					'thumb',				// Thumbnail properties object
3312			IMAGE: 					'image',				// Image properties object
3313			WIDTH: 					'width',				// Width
3314			HEIGHT: 				'height',				// Height
3315			RENDITIONS:				'renditions',			// Variant renditions
3316			ORIGINAL: 				'original',				// Original object
3317			OBJECTS: 				'objects',				// Objects array (not folders)
3318			FILEDATE: 				'fileDate',				// File modified date
3319			COMMENT: 				'comment',				// Comment
3320			TITLE: 					'title',				// Title
3321			KEYWORDS:				'keywords',				// Keywords
3322			COUNTERS: 				'counters',				// Counters object
3323			DEEPCOUNTERS: 			'deepCounters',			// Deepcounters object
3324			FILESIZE: 				'fileSize',				// File size (int)
3325			CATEGORY: 				'category',				// Category
3326			RATING:					'rating',				// Rating
3327			PRICE:					'price',				// Price		
3328			CAMERA: 				'camera',				// Camera data object
3329			VIDEO: 					'video',				// Video data object
3330			DURATION:				'duration',				// Video duration
3331			FPS:					'fps',					// Video frame per sec.
3332			HIDDEN:					'hidden',				// Hidden
3333			VIEW:					'view',					// View type in case the folder is a view
3334			INTERNALLINK:			'internalLink',			// Internally linked object
3335			// extra vars
3336			LEVEL: 					'level',				// Folder depth level
3337			PATHREF: 				'pathRef',				// Path from root
3338			PARENTREF: 				'parentRef',			// Path to parent from root
3339			RELPATH: 				'relPath',				// Relative path from currentFolder to this folder 
3340			FOLDERCAPTION:			'folderCaption',		// Folder caption derived from template
3341			IMAGECAPTION: 			'imageCaption',			// Image caption derived from template
3342			THUMBCAPTION: 			'thumbCaption',			// Thumbnail caption derived from template
3343			PHOTODATA: 				'photodata',			// Formatted photo data
3344			LOCATION: 				'location',				// GPS location
3345			HIDELOCATION:			'hideLocation',			// Hide GPS location?
3346			DISABLEFEEDBACK:		'disableFeedback',		// Hide from Feedback?
3347			REGIONS:				'regions',				// Regions (face tags)
3348			SHOP:					'shop',					// Global or individual shop options
3349			EXTERNAL:				'external',				// External (link or content)
3350			PROJECTIONTYPE:			'projectionType',		// 3D image projection type (equirectangular)
3351			GPANO:					'gpano',				// Globe panorama (360°)
3352			ORIGINALFILE:			'originalFile',			// Path to the original file in case of GIF's
3353			ORIGINALPAGENAME:		'originalPageName',		// The original name of a custom page template (before renamed)
3354			EMBEDDED:				'embedded',				// Custom page is embedded?
3355			CUSTOMPAGECONTENT:		'customPageContent',	// Custom page content (used when pages displayed in the lightbox)
3356			SLIDESHOWINTERVAL:		'slideshowInterval',	// Custom slideshow delay
3357			TRANSITIONSPEED:		'transitionSpeed',		// Custom transotoin speed
3358			DATES:					'dates',				// Dates object
3359			ADDED:					'added',				// Added date
3360			DATETAKEN:				'dateTaken',			// Date taken
3361			FILEMODIFIED:			'fileModified',			// File modified
3362			DATERANGE:				'dateRange',			// Date range (folders)
3363			MOSTPHOTOS: 			'mostphotos',			// Mostphotos ID
3364			HIDEFOTOMOTO:			'hideFotomoto',			// Hide from Fotomoto
3365			FOTOMOTOCOLLECTION:		'fotomotoCollection',	// Fotomoto collection type
3366			AUDIOCLIP:				'audioClip',			// Attached audio clip (mp3)
3367			PANORAMA:				'panorama',				// To be treated as panorama?
3368			FILTERS:				'filters',				// Filters array (global or folders)
3369			SORT:					'sort',					// Sort array (global or folders)
3370			VISITORRATING:			'visitorRating',		// Ratings from jAlbum
3371			OBJ: 					'obj',					// Store item as data attr of an element: el.data(J.OBJ)  
3372			LOADCOUNTER:			'loadcounter',			// Load counter array by category
3373			TOTAL:					'total',				// Total items loaded
3374			FOLDERINDEX:			'folderindex',			// Numbering folders: 0 ... N
3375			READY:					'ready',				// Data has been loaded
3376			ONREADY:				'onReady',				// Cached functions for data loaded event
3377			DEEP:					'deep',					// Using tree or deep-data structure?
3378			SIZE:					'size',					// External content embed size 
3379			CONT:					'cont',					// External content
3380			TYPE:					'type'					// E.g. for audioClip.type
3381		},
3382	
3383	JCAMERAFIELDS = [
3384			'aperture',
3385			'exposureTime',
3386			'originalDate',
3387			'cameraModel',
3388			'location',
3389			'focusDistance',
3390			'focalLength35mm',
3391			'cameraMake',
3392			'resolution',
3393			'isoEquivalent',
3394			'flash',
3395			'focalLength'
3396		]; 
3397
3398/*
3399 *	Album object :: use 
3400 *		myAlbum = new Album({settings});
3401 *		or
3402 *		myAlbum = new Album();
3403 *		myAlbum.init({settings});
3404 */
3405
3406var Album = function($, options) {
3407	
3408	let instance = null,
3409	
3410		settings = {
3411				// Name of the tree file
3412				treeFile: 				'tree.json',
3413				// Name of the folder data
3414				dataFile: 				'data1.json',
3415				// Name of the master file
3416				deepDataFile:			'deep-data.json',
3417				// index file name
3418				indexName: 				'index.html',
3419				//Folder image file name
3420				folderImageFile:		'folderimage.jpg',
3421				// Folder image dimensions
3422				folderImageDims:		[ 1200, 800 ],
3423				// Folder thumbnail file name 
3424				folderThumbFile:		'folderthumb.jpg',
3425				// Folder thumbnail dimanesions
3426				folderThumbDims:		[ 1024, 768 ],
3427				// Thumbnail dimensions
3428				thumbDims:				[ 240, 180 ],
3429				// Name of the thumbs folder
3430				thumbsDir: 				'thumbs',
3431				// Name of the slides folder
3432				slidesDir: 				'slides',
3433				// Name of the hires folder
3434				hiresDir:				'hi-res',
3435				// High DPI thumbnails
3436				hiDpiThumbs:			false,
3437				// High DPI images
3438				hiDpiImages:			false,
3439				// Use originals?
3440				useOriginals:			false,
3441				// Do thumbs fill the frame or fit 
3442				thumbsFillFrame:		true,
3443				// Default poster images
3444				audioPoster:			'audio.poster.png',
3445				defaultAudioPosterSize:	[ 628, 360 ],
3446				videoPoster:			'video.poster.png',
3447				defaultVideoPosterSize:	[ 628, 360 ],
3448				// Path to root from current folder (eg. "../../")
3449				rootPath: 				'',
3450				// Relative path to the current folder (eg. "folder/subfolder") 
3451				relPath: 				'',
3452				// Loading the whole data tree
3453				loadDeep:				false,
3454				// Lazy load :: loads folder data only when requested or at the time of initialization
3455				lazy: 					true,
3456				// Show pages in the Lightbox?
3457				showPagesInLightbox:	false,
3458				// Fatal error handling
3459				fatalError:				(txt, attr) => log('Error: ' + ((typeof attr === UNDEF)? translate(txt) : translate(txt).replace(/\{0\}/g, attr))),
3460				// Possible object types
3461				possibleTypes:			[ 'folder', 'webPage', 'webLocation', 'image', 'video', 'audio', 'other' ],
3462				locale:					'en-US'
3463			},
3464			
3465		// Texts translated
3466		text = getTranslations({
3467				and:							"and",
3468				from:							"From {0}"
3469			}),
3470		// Creating RegExp with localized AND
3471		andRegExp = new RegExp(`\\s+(?:&|${text.and})\\s+`, 'gu'),
3472		// Global variables
3473		// URL of to top level album page (null => we're in the album's subfolder, using the relative "rootPath") 
3474		albumPath = null,
3475		// Absolute URL of the top level page
3476		absolutePath,
3477		// Cache buster
3478		cacheBuster = '',
3479		// The container for the entire tree
3480		tree,
3481		// Collection of album paths from root in order to store only the references
3482		paths = [],
3483		// Collection of relative paths in order to store only the references
3484		relPaths = [],
3485		// Path to current folder
3486		currentFolder,
3487		// Collection the JSON promises
3488		defer = [],
3489		// Album ready state: tree.json and data1.json is loaded
3490		ready = false,
3491		// Deep ready: all the data structure is ready
3492		deepReady = false,
3493		// Collecting deep ready callbacks to execute on deep ready event
3494		deepReadyFunctions = [],
3495		// Is deep ready?
3496		isDeep = () => (tree && tree.hasOwnProperty(J.DEEP) && tree[J.DEEP]),
3497		
3498		/***********************************************
3499		 *					Debug
3500		 */
3501		 
3502		// Logging
3503		scriptName = 'jalbum-album.js',
3504	
3505		log = txt => {
3506			
3507				if (console && typeof txt !== UNDEF) {
3508					
3509					if (txt.match(/^Error\:/i)) {
3510						console.error(scriptName + ' ' + txt);
3511					}
3512					
3513					if (typeof DEBUG !== UNDEF && DEBUG) {
3514						if (txt.match(/^Warning\:/i)) {
3515							console.warn(scriptName + ' ' + txt);
3516						} else if (txt.match(/^Info\:/i)) {
3517							console.info(scriptName + ' ' + txt);
3518						} else {
3519							console.log(scriptName + ' ' + txt);
3520						}
3521					}
3522				}
3523			},
3524				
3525		// Utility functions
3526		getFilenameFromURL = n => decodeURIComponent(n.slice(n.lastIndexOf('/') + 1)),
3527		
3528		// File name without extension
3529		getBasename = o => { 
3530				var m = getItemName(o).match(/^(.+)\./); 
3531				return m? m[1] : '';
3532			},
3533				
3534		// File extension
3535		getExtension = o => { 
3536				var m = getItemName(o).match(/\.(\w+)$/); 
3537				return m? m[1] : '';
3538			},
3539				
3540		// Can the browser display this image type?
3541		isBrowserFriendly = n => {
3542				var m = n.match(/\.\w+$/);
3543				return m? ('.jpg.jpeg.png.webp.webm.gif'.indexOf(m[0].toLowerCase()) !== -1) : false;
3544			},
3545			
3546		/***********************************************
3547		 *					Type check
3548		 */
3549		 
3550		// Image?
3551		isImage = o =>
3551 o.hasOwnProperty(J.CATEGORY) && o[J.CATEGORY] === 'image',
3552			
3553		// Audio?
3554		isAudio = o => o.hasOwnProperty(J.CATEGORY) && o[J.CATEGORY] === 'audio',
3555			
3556		// Video?
3557		isVideo = o => o.hasOwnProperty(J.CATEGORY) && o[J.CATEGORY] === 'video',
3558			
3559		// Page?
3560		isPage = o => o.hasOwnProperty(J.CATEGORY) && o[J.CATEGORY] === 'webPage',
3561			
3562		// Folder?
3563		isFolder = o => o.hasOwnProperty(J.FOLDERINDEX) || o.hasOwnProperty(J.LEVEL),
3564		
3565		// IsView
3566		isView = o => o.hasOwnProperty(J.VIEW),
3567			
3568		// Lightboxable custom pages
3569		lightboxablePage = [ 'about', 'contact', 'contents', 'sitemap' ],
3570			
3571		// Ordinary object?
3572		isLightboxable = o => (o.hasOwnProperty(J.CATEGORY) && 
3573				(	o[J.CATEGORY] === 'image' || 
3574					o[J.CATEGORY] === 'video' || 
3575					o[J.CATEGORY] === 'audio' ||
3576					o[J.CATEGORY] === 'other' || 
3577					settings.showPagesInLightbox && 
3578						o[J.CATEGORY] === 'webPage' && 
3579						(!o.hasOwnProperty(J.ORIGINALPAGENAME) || lightboxablePage.includes(o[J.ORIGINALPAGENAME])) 
3580				)
3581			),
3582	
3583		// Is this the current folder?
3584		isCurrentFolder = o => o === currentFolder,
3585			
3586		/***********************************************
3587		 *					Access
3588		 */
3589		 
3590		// Returns path separated with separator 
3591		/*	'': 			current, 
3592			'/': 			album root, 
3593			'folderName': 	subfolder,
3594			'name1/name2':	deep folder
3595		*/
3596		
3597		makePath = function() {
3598			
3599				if (arguments.length) {
3600					let p = [];
3601					
3602					for (let i = 0, a; i < arguments.length; i++) {
3603						a = arguments[i];
3604						
3605						if (!a.length) {
3606							// Can be empty
3607							continue;
3608						}
3609						
3610						if (a === '/') {
3611							// Return to root
3612							p = [];
3613						} else {
3614							// Removing trailing slashes if any
3615							if (a[0] === '/') {
3616								a = a.slice(1);
3617							}
3618							if (a.slice(-1) === '/') {
3619								a = a.slice(0, -1);
3620							}
3621							
3622							if (a.length) {
3623								// Adding segment
3624								p.push(a);
3625							}
3626						}
3627					}
3628						
3629					return p.join('/');
3630				}
3631				
3632				return '';
3633			},
3634			
3635		// Get absolute path to folder from relative folder path
3636		
3637		getAbsoluteFolderPath = path => {
3638			
3639				if (typeof path === UNDEF) {
3640					return window.location.href.substring(0, window.location.href.lastIndexOf('/'));
3641				} else if (path.match(/^https?\:\/\//i)) {
3642					// Absolute
3643					return path;
3644				} else if (path[0] === '/') {
3645					// Site root relative
3646					return window.location.origin + path;
3647				} else {
3648					let url = window.location.href;
3649					// Find last '/'
3650					url = url.substring(0, url.lastIndexOf('/'));
3651					// Go back until ../
3652					if (path.endsWith('..')) {
3653						// Ensure to avoid ".." ending
3654						path += '/';
3655					}
3656					while (path.startsWith('../')) {
3657						url = url.substring(0, url.lastIndexOf('/', url.length - 2));
3658						path = path.slice(3);
3659					}
3660					return url + (path? '/' + path : '');	// ???
3661				}
3662			},
3663			
3664		// Getting path reference either by searching or adding as new
3665		// 0 => top level folder
3666		
3667		getPathRef = p => {
3668			
3669				if (typeof p === UNDEF || !p) {
3670					// Top level folder
3671					return 0;
3672				}
3673			
3674				if (p.slice(-1) !== '/') {
3675					// Sanitizing paths: adding extra slash at the end 
3676					p += '/';
3677				}
3678				
3679				let idx = paths.indexOf(p);
3680				
3681				if (idx >= 0) {
3682					// Found => return
3683					return idx + 1;
3684				}
3685				
3686				// Add new
3687				return paths.push(p);
3688			},
3689			
3690		// Reverse function
3691		
3692		toPath = o => (o && o.hasOwnProperty(J.PATHREF) && o[J.PATHREF])? paths[o[J.PATHREF] - 1] : '',
3693		
3694		// Getting path reference either by searching or adding as new
3695		// 0 => current folder
3696		
3697		getRelPathRef = p => {
3698			
3699				if (typeof p === UNDEF || !p) {
3700					// Current folder
3701					return 0;
3702				}
3703			
3704				if (p.slice(-1) !== '/') {
3705					// Sanitizing paths: adding extra slash at the end 
3706					p += '/';
3707				}
3708				
3709				let idx = relPaths.indexOf(p);
3710				
3711				if (idx >= 0) {
3712					// Found => return
3713					return idx + 1;
3714				}
3715				
3716				// Add new
3717				return relPaths.push(p);
3718			},
3719		
3720		// Reverse function
3721		
3722		toRelPath = o => (o && o.hasOwnProperty(J.RELPATH) && o[J.RELPATH])? relPaths[o[J.RELPATH] - 1] : '',	
3723		
3724		// Fixing path containing ../ part(s)
3725		
3726		fixUrl = p => {
3727				let i, 
3728					j, 
3729					t = p + '';
3730					
3731				while ((i = t.indexOf('../')) > 0) {
3732					if (i === 1 || (j = t.lastIndexOf('/', i - 2)) === -1) {
3733						return t.substring(i + 3);
3734					}
3735					t = t.substring(0, j) + t.substring(i + 2);
3736				}
3737				
3738				return t;
3739			},
3740			
3741		// Get absolute URL to album's root folder
3742		
3743		getAlbumPath = () => {
3744			
3745				if (albumPath !== null) {
3746					return albumPath;
3747				}
3748				
3749				let p = window.location.pathname,
3750					l = currentFolder[J.LEVEL];
3751				
3752				do {
3753					p = p.substring(0, p.lastIndexOf('/'));
3754					l = l - 1;
3755				} while (l >= 0);
3756				
3757				return p;
3758			},
3759			
3760		// Get album root folder
3761		
3762		getAlbumRootPath = () => absolutePath,
3763		
3764		// Gets the relative path to element for linked objects
3765		
3766		getLinkTo = o => {
3767				let p,
3768					d;
3769					
3770				if (o.hasOwnProperty(J.THUMB)) {
3771					p = o[J.THUMB][J.PATH];
3772					d = settings.thumbsDir + '/';
3773				} else if (o.hasOwnProperty(J.IMAGE)) {
3774					p = o[J.IMAGE][J.PATH];
3775					d = settings.slidesDir + '/';
3776				} else {
3777					return '';
3778				}
3779				
3780				if (p.startsWith(d)) {
3781					return '';
3782				}
3783				
3784				return p.slice(0, p.indexOf('/' + d) + 1);
3785			},
3786			
3787		// Getting folder path for an object (absolute or relative)
3788		
3789		getPath = o => {
3790			
3791				if (typeof o !== UNDEF) {
3792					return (albumPath !== null)? makePath(albumPath, toPath(o)) : toRelPath(o);
3793				}
3794				// Top level or external reference to top level
3795				return albumPath || '';
3796			},
3797			
3798		// Getting path from album root to the item's folder
3799		
3800		getFolderPath = o => (typeof o !== UNDEF)? toPath(o) : '',
3801		
3802		// Getting absolute path for an object
3803		
3804		getAbsolutePath = o => (
3805				o.hasOwnProperty(J.LEVEL)?
3806					// Folder
3807					makePath(albumPath || absolutePath, getFolderPath(o))
3808					:
3809					// Object
3810					makePath(albumPath || absolutePath, getFolderPath(o), settings.indexName + '#img=' + o[J.PATH])
3811			),
3812			
3813		// Gets the path to item: absolute or relative
3814		
3815		getItemPath = o => {
3816				if (typeof o !== UNDEF) {
3817					let p = getPath(o),
3818						c = o[J.CATEGORY] || 'folder';
3819					
3820					if (c === 'folder') {
3821						return p;
3822					} else if (c === 'video') {
3823						return makePath(p, o[J.VIDEO][J.PATH]);
3824					} else if (c === 'audio' || c === 'other' || o.hasOwnProperty(J.ORIGINAL)) {
3825						return makePath(p, o[J.ORIGINAL][J.PATH]);
3826					} else if (c === 'image') {
3827						return makePath(p, o[J.IMAGE][J.PATH]);
3828					} else if (c === 'webPage') {
3829						return makePath(p, o[J.PATH]);
3830					} else {
3831						// webLocation: absolute
3832						return o[J.PATH];
3833					}
3834				}
3835				
3836				return null;
3837			},
3838			
3839		// Gets the audioclip path
3840		
3841		getAudioClipPath = o => 
3842				(typeof o !== UNDEF && o.hasOwnProperty(J.AUDIOCLIP))?
3843					makePath(getPath(o), o[J.AUDIOCLIP][J.PATH]) 
3844					: 
3845					null,
3846			
3847		// Getting path to object from album root
3848		
3849		getObjectPath = o => 
3850				(typeof o !== UNDEF)? 
3851					(toPath(o) + ((o[J.CATEGORY] !== 'folder')? o[J.PATH] : '')) 
3852					: 
3853					null,
3854			
3855		// Tries to figure out the items dimensions
3856		
3857		getDimensions = o => {
3858				
3859				if (typeof o === UNDEF || o[J.CATEGORY] === 'webPage' || o[J.CATEGORY] === 'other' && getExtension(o).toLowerCase() === 'pdf') {
3860					// No (valid) object or has no dimensions
3861					return null;
3862				} else if (o.hasOwnProperty(J.EXTERNAL)) {
3863					// External content
3864					let s = o[J.EXTERNAL][J.SIZE];
3865					
3866					if (s) {
3867						// Has manually specified size
3868						s = s.split('x');
3869						return [ s[0], s[1] || Math.round(s[0] *.75)];
3870						
3871					} else {
3872						s = guessDimensions(o[J.EXTERNAL][J.CONT]);
3873						if (s) {
3874							// Has explicitly given size in the HTML code
3875							return s;
3876						} else {
3877							// No info
3878							if (o[J.EXTERNAL][J.CONT].includes('vimeo.com') ||
3879								o[J.EXTERNAL][J.CONT].includes('youtube.com') ||
3880								o[J.EXTERNAL][J.CONT].includes('youtu.be')) {
3881								// In case of YouTube or Vimeo, set 720p as default
3882								if ((s = o[J.EXTERNAL][J.CONT].match(/.*style="padding(-bottom)?:\s?([\d\.]+)%/)) && s && (s.length > 1)) {
3883									// Responsive embedding
3884									return [ 1280, Math.round((w * parseFloat(s[2])) / 100) ];
3885								} else {
3886									return [ 1280, 720 ];
3887								}
3888							} else {
3889								return [ o[J.IMAGE][J.WIDTH], o[J.IMAGE][J.HEIGHT] ];
3890							}
3891						}
3892					}
3893				} else if (o[J.CATEGORY] === 'audio' && o[J.IMAGE][J.PATH].endsWith('res/audio.png')) {
3894					// Audio with poster image
3895					return [ settings.defaultAudioPosterSize[0], settings.defaultAudioPosterSize[1] ];
3896					
3897				} else if (o[J.CATEGORY] === 'video') {
3898					// Video
3899					return [ o[J.VIDEO][J.WIDTH], o[J.VIDEO][J.HEIGHT] ];
3900					
3901				}
3902				
3903				if (settings.hiDpiImages) {
3904					return [ o[J.IMAGE][J.WIDTH] / 2, o[J.IMAGE][J.HEIGHT] / 2 ];
3905				}
3906				
3907				return [ o[J.IMAGE][J.WIDTH], o[J.IMAGE][J.HEIGHT] ];
3908			},
3909			
3910		// Tries to figure out the original image dimensions
3911		
3912		getOriginalDimensions = o => {
3913			
3914				if (typeof o !== UNDEF) {
3915				
3916					if (o[J.CATEGORY] === 'video') {
3917						// Video
3918						return [ o[J.VIDEO][J.WIDTH], o[J.VIDEO][J.HEIGHT] ];
3919					}
3920					
3921					if (o.hasOwnProperty(J.ORIGINAL)) {
3922						return [ o[J.ORIGINAL][J.WIDTH], o[J.ORIGINAL][J.HEIGHT] ];
3923					}
3924				}
3925				
3926				return null;
3927			},
3928			
3929		// Tries to figure out the original image dimensions
3930		
3931		getMaxDimensions = o => {
3932			
3933				if (typeof o !== UNDEF) {
3934					let dim;
3935					
3936					if (settings.useOriginals && (dim = getOriginalDimensions(o))) {
3937						// Has original
3938						return dim;
3939					} else if (o.hasOwnProperty(J.IMAGE)) {
3940						// Has image
3941						let img = o[J.IMAGE];
3942						
3943						if (img.hasOwnProperty(J.RENDITIONS)) {
3944							// Has renditions (variants)
3945							dim = [ img[J.RENDITIONS][0][J.WIDTH], img[J.RENDITIONS][0][J.HEIGHT] ];
3946						
3947							for (let i = 1; i < img[J.RENDITIONS].length; i++) {
3948								if (img[J.RENDITIONS][i][J.WIDTH] > dim[0]) {
3949									dim = 	[ img[J.RENDITIONS][i][J.WIDTH], img[J.RENDITIONS][i][J.HEIGHT] ];
3950								}
3951							}
3952							
3953							return dim;
3954							
3955						}
3956							
3957						return settings.hiDpiImages?
3958								[ 2 * img[J.WIDTH], 2 * img[J.HEIGHT] ]
3959								:
3960								[ img[J.WIDTH], img[J.HEIGHT] ];
3961					}
3962				}
3963				
3964				return null;
3965			},
3966			
3967		// Returns the thumbnail's aspect ratio
3968		
3969		getAspectRatio = function(o, thumb) {
3970				
3971				if (typeof o !== UNDEF) {
3972					if (typeof thumb === UNDEF) thumb = false;
3973					if (thumb && o.hasOwnProperty(J.THUMB)) {
3974						return o[J.THUMB][J.WIDTH] / o[J.THUMB][J.HEIGHT];
3975					}
3976					if (!thumb) {
3977						if (o[J.CATEGORY] === 'video') {
3978							return o[J.VIDEO][J.WIDTH] / o[J.VIDEO][J.HEIGHT];
3979						} else if (o.hasOwnProperty(J.IMAGE)) {
3980							return o[J.IMAGE][J.WIDTH] / o[J.IMAGE][J.HEIGHT];
3981						}
3982					}
3983				}
3984				
3985				return 'auto';
3986			},
3987			
3988		// Returns the parsed regions array
3989		
3990		getRegions = function(o) {
3991			
3992				if (typeof o !== UNDEF && o.hasOwnProperty(J.REGIONS)) {
3993					return JSON.parse(o[J.REGIONS]);
3994				}
3995				
3996				return [];
3997			},
3998			
3999		// Returns absolute or relative link to object in the real album
4000		
4001		getLink = function(o) {
4002			
4003				if (typeof o !== UNDEF) {
4004					
4005					switch (o[J.CATEGORY]) {
4006						
4007						case 'folder':
4008							
4009							return getPath(o);
4010							
4011						case 'webLocation':
4012							
4013							return o[J.PATH];
4014							
4015						case 'webPage':
4016							
4017							return makePath(getPath(o), o[J.PATH]);
4018							
4019						default:
4020							
4021							// Image or other lightboxable item
4022							return makePath(getPath(o), '#img=' + o[J.PATH]);
4023					}
4024				}
4025				
4026				return null;
4027			},
4028							
4029		// Get path to item from root
4030		
4031		getRootPath = o => (typeof o !== UNDEF)? makePath(getPath(o), o[J.PATH]) : null,
4032			
4033		// Get the pointer to a folder in a tree from path reference number
4034		
4035		getPointer = n => {
4036			
4037				if (typeof n !== NUMBER || n <= 0) {
4038					// root
4039					return tree;
4040				}
4041				
4042				n--;
4043				
4044				if (n > paths.length) {
4045					log('Error: out of bounds path reference (' + n + ')!');
4046					return null;
4047				}
4048				
4049				return getFolder(paths[n]);
4050			},
4051				
4052		// Gets relative folder path from the current folder (unnecessary)
4053		
4054		getRelativeFolderPath = o => (typeof o !== UNDEF)? fixUrl(settings.rootPath + toPath(o)) : null,
4055					
4056		// Returns folder object by full path
4057		
4058		getFolder = path => {
4059				
4060				if (typeof path === UNDEF) {
4061					// Invalid
4062					return null;
4063				} else if (!path.length) {
4064					// Top level
4065					return tree;
4066				}
4067				
4068				if (path.endsWith('/')) {
4069					// Sanitize
4070					path = path.slice(0, -1);
4071				}
4072				if (path.startsWith('/')) {
4073					// Sanitize
4074					path = path.slice(1);
4075				}
4076				
4077				let f = tree,
4078					p = path.split('/'),
4079					level;
4080					
4081				for (level = 0; level < p.length; level++) {
4082					if (isDeep()) {
4083						if (f.hasOwnProperty(J.OBJECTS) && 
4084							(f = f[J.OBJECTS].find(o => (o[J.CATEGORY] === 'folder' && o[J.PATH] === p[level])))) {
4085							continue;
4086						}
4087					}
4087 else if (f.hasOwnProperty(J.FOLDERS) &&
4088						(f = f[J.FOLDERS].find(o => o[J.PATH] === p[level]))) { 
4089						continue;
4090					}
4091					// Not found: return null 
4092					return null;
4093				}
4094				
4095				// Matched full path
4096				return (level === p.length)? f : null;
4097			},
4098			
4099		// Returns current folder
4100		
4101		getCurrentFolder = () => currentFolder,
4102		
4103		// Getting the parent
4104		
4105		getParent = o => {
4106			
4107				if (typeof o === UNDEF) {
4108					// Invalid
4109					o = currentFolder;
4110				}
4111				
4112				if (o === tree) {
4113					// At top level
4114					return null;
4115				}
4116				
4117				// Getting current folder or the parent in case of a folder
4118				let p = o.hasOwnProperty(J.PARENTREF)? getPointer(o[J.PARENTREF]) : getPointer(o[J.PATHREF]);
4119				
4120				// Avoid endless loops
4121				return (p === o)? null : p;
4122			},
4123			
4124		// Getting an item from full path, waits for folder to load if not present
4125		// Optionally passing an extra parameter
4126		
4127		getItem = function(path, doneFn, p) {
4128				
4129				if (typeof doneFn !== FUNCTION) {
4130					return;
4131				}
4132				
4133				let _getItem = function(folder, args) {
4134							let name = args[0],
4135								doneFn = args[1];
4136								
4137							if (folder.hasOwnProperty(J.OBJECTS)) {
4138								let o;
4139								
4140								if (name.endsWith('.webloc')) {
4141									// Filtering for "slides/NAME.ext"
4142									name = name.slice(0, -7);
4143									o = folder[J.OBJECTS].find(function(o) {
4144											return o[J.CATEGORY] === 'webLocation' && o[J.IMAGE][J.PATH].slice(7).stripExt() === name;
4145										});
4146								} else {
4147									o = folder[J.OBJECTS].find(function(o) {
4148											return o.hasOwnProperty(J.ORIGINAL) && o[J.ORIGINAL][J.PATH] === name ||		// Original path (e.g. Other file types
4149												o.hasOwnProperty(J.IMAGE) && (
4150														o[J.IMAGE][J.PATH] === name ||										// Non-converted images, e.g. GIF or PNG
4151														o[J.IMAGE][J.PATH] === SLIDES_DIR + '/' + name						// Normal images
4152													) ||
4153												o[J.PATH] === name;															// Fallback to Path
4154										});
4155								}
4156								
4157								if (typeof o === UNDEF) {
4158									//log('Missing object "' + name +  '"');
4159									o = null;
4160								}
4161								
4162								if (typeof p !== UNDEF) {
4163									doneFn.call(o, p);
4164								} else {
4165									doneFn.call(o);
4166								}
4167							} else {
4168								//log('No "Objects" in folder "' + folder[J.NAME] + '"');
4169							}
4170						};
4171						
4172				if (!path) {
4173					// Tree
4174					if (typeof p !== UNDEF) {
4175						doneFn.call(tree, p);
4176					} else {
4177						doneFn.call(tree);
4178					}
4179				} else if (path.endsWith('/')) {
4180					// Folder
4181					if (typeof p !== UNDEF) {
4182						doneFn.call(getFolder(path), p);
4183					} else {
4184						doneFn.call(getFolder(path));
4185					}
4186				} else {
4187					// An object
4188					let i = path.lastIndexOf('/'),
4189						folder = (i === -1)? tree : getFolder(path.substring(0, i)),
4190						name = path.substring(i + 1);
4191					
4192					if (folder) {
4193						// Exists
4194						if (deepReady || folder.hasOwnProperty(J.READY) && folder.ready) {
4195							// Objects already loaded
4196							//log('Accessing "' + name + '" in folder "' + folder[J.NAME] + '"');
4197							_getItem(folder, [ name, doneFn ]);
4198							
4199						} else if (isDeep()) {
4200							// Loads deep, but database hasn't been loaded yet
4201							//log('Not yet deep ready when accessing "' + name + '" in folder "' + folder[J.NAME] + '"');
4202							onDeepReady(folder, _getItem, [ name, doneFn ]);
4203						} else {
4204							// Have to wait until objects get loaded
4205							//log('Folder "' + folder[J.NAME] + '" is not yet ready when accessing "' + name + '"');
4206							onFolderReady(folder, _getItem, [ name, doneFn ]);
4207						}
4208					}
4209				}
4210				
4211				return null;
4212			},
4213		
4214		// Returns all objects in a folder but subfolders
4215		
4216		getObjects = f => {
4217				let folder = (typeof f === UNDEF)? currentFolder : f,
4218					objects = [];
4219					
4220				if (folder.hasOwnProperty(J.OBJECTS)) {
4221					folder[J.OBJECTS].forEach(o => {
4222							if (o[J.CATEGORY] !== 'folder') {
4223								objects.push(o);
4224							}
4225						});
4226				}
4227				
4228				return objects;
4229			},
4230			
4231		// Returns all objects in a folder
4232		
4233		getAllObjects = f => {
4234				let folder = (typeof f === UNDEF)? currentFolder : f,
4235					objects = [];
4236					
4237				if (folder.hasOwnProperty(J.OBJECTS)) {
4238					folder[J.OBJECTS].forEach(o => {
4239							if (o.hasOwnProperty(J.FOLDERINDEX)) {
4240								objects.push(folder[J.FOLDERS][o[J.FOLDERINDEX]]);
4241							} else {
4242								objects.push(o);
4243							}
4244						});
4245				}
4246				
4247				return objects;
4248			},
4249			
4250		// Returns folders and weblocations
4251		
4252		getFoldersAndWeblocs = f => {
4253				let folder = (typeof f === UNDEF)? currentFolder : f,
4254					objects = [];
4255					
4256				if (folder.hasOwnProperty(J.OBJECTS)) {
4257					folder[J.OBJECTS].forEach(o => {
4258							if (o.hasOwnProperty(J.FOLDERINDEX)) {
4259								objects.push(folder[J.FOLDERS][o[J.FOLDERINDEX]]);
4260							} else if (o[J.CATEGORY] === 'folder' || o[J.CATEGORY] === 'webLocation') {
4261								objects.push(o);
4262							}
4263						});
4264				}
4265				
4266				return objects;
4267			},
4268			
4269		// Returns only the lightboxable items
4270		
4271		getImages = f => {
4272				let folder = (typeof f === UNDEF)? currentFolder : f,
4273					objects = [];
4274			
4275				if (folder.hasOwnProperty(J.OBJECTS)) {
4276					folder[J.OBJECTS].forEach(o => {
4277							if (isLightboxable(o)) {
4278								objects.push(o);
4279							}
4280						});
4281				}
4282							
4283				return objects;
4284			},
4285		
4286		// Returns the folders as an array. No hidden folders. Avoid copying the child folders.
4287		
4288		getFolders = f => {
4289				let folder = (typeof f === UNDEF)? currentFolder : f,
4290					folders = [],
4291					
4292					// Avoid copying deep structure; only the immediate properties
4293					copyProps = f => {
4294							let o = {};
4295							
4296							for (let prop in f) {
4297								// Copying folder variables besides OBJECTS and ALBUM (only those missing)
4298								if (prop !== J.OBJECTS && prop !== J.ALBUM && prop !== J.FOLDERS) {
4299									o[prop] = f[prop];
4300								}
4301							}
4302							
4303							return o;
4304						};	
4305							
4306				if (folder) {
4307					if (isDeep()) {
4308						if (folder.hasOwnProperty(J.OBJECTS)) {
4309							folder[J.OBJECTS].forEach(o => {
4310									if (o[J.CATEGORY] === 'folder' && !(o.hasOwnProperty(J.HIDDEN) && o.hidden)) {
4311										folders.push(copyProps(o));
4312									}
4313								});
4314						}
4315					} else if (folder.hasOwnProperty(J.FOLDERS)) {
4316						folder[J.FOLDERS].forEach(f => {
4317								if (!(f.hasOwnProperty(J.HIDDEN) && f.hidden)) {
4318									folders.push(copyProps(f));
4319								}
4320							});
4321					}
4322				}
4323							
4324				return folders;
4325			},
4326			
4327		/***********************************************
4328		 *
4329		 *				Properties
4330		 *
4331		 */
4332		 
4333		// Album make date/time in UTC
4334		
4335		getMakeDate = () => new Date(tree[J.FILEDATE]),
4336			
4337		// Album title
4338		
4339		getAlbumTitle = () => tree[J.TITLE] || tree[J.NAME],
4340			
4341		// Filename for images, originalname for other
4342		
4343		getItemName = o => (
4344				(o[J.CATEGORY] === 'video')?
4345					getFilenameFromURL(o[J.VIDEO][J.PATH])
4346					:
4347					(o.hasOwnProperty(J.ORIGINAL)? 
4348						getFilenameFromURL(o[J.ORIGINAL][J.PATH])
4349						:
4350						o[J.NAME]
4351					)
4352			),
4353		
4354		// Level?
4355		
4356		getLevel = o => {
4357				o = o || currentFolder;
4358				return o.hasOwnProperty(J.LEVEL)? o[J.LEVEL] : getLevel(getParent(o));
4359			},
4360						
4361		// Title
4362		
4363		getTitle = o => (o || currentFolder)[J.TITLE] || '',
4364			
4365		// Name
4366		
4367		getName = o => (o || currentFolder)[J.NAME] || '',
4368			
4369		// Label: underscores replaced with spaces, file extension removed
4370		
4371		getLabel = o => ((o || currentFolder)[J.NAME] || '').replace(/\.\w+$/, '').replace(/_/g, ' '),
4372			
4373		// Title or name for ALT tags
4374		
4375		getAlt = o => getTitle(o) || getLabel(o),
4376		
4377		// Comment
4378		
4379		getComment = o => (o || currentFolder)[J.COMMENT] || '',
4380			
4381		// Thumbnail path (folder thumbs located one level up!)
4382		
4383		getThumbPath = o => makePath(getPath(o), 
4384				o.hasOwnProperty(J.LEVEL)?
4385					// Folder
4386					o[J.THUMB][J.PATH].replace(o[J.PATH] + '/', '')
4387					:
4388					o[J.THUMB][J.PATH]
4389			),
4390			
4391		// Icon path?
4392		
4393		getIconPath = o => o[J.THUMB][J.PATH].match(/res\/\w+\.png$/)? o[J.THUMB][J.PATH] : '',
4394		
4395		// Image path (falls back to thumb path, e.g. PDF files)
4396		
4397		getImagePath = o => makePath(getPath(o), 
4398				o.hasOwnProperty(J.LEVEL)?
4399					// Folder: grab the same image as thumb from slides
4400					o[J.THUMB][J.PATH].replace(settings.thumbsDir + '/', settings.slidesDir + '/') 
4401					:
4402					// Image or other
4403					(o.hasOwnProperty(J.ORIGINALFILE)?
4404						o[J.ORIGINALFILE]
4405						:
4406						(o.hasOwnProperty(J.IMAGE)? 
4407							o[J.IMAGE][J.PATH] 
4408							: 
4409							o[J.THUMB][J.PATH]
4410						)
4411					)
4412			),
4413		
4414		// Image path from root
4415		
4416		getImagePathFromRoot = o => makePath(toPath(o), 
4417				o.hasOwnProperty(J.ORIGINALFILE)?
4418					o[J.ORIGINALFILE]
4419					:
4420					(o.hasOwnProperty(J.IMAGE)? 
4421						o[J.IMAGE][J.PATH] 
4422						: 
4423						o[J.THUMB][J.PATH]
4424					)
4425			),
4426		
4427		// Absolute image path
4428		
4429		getAbsoluteImagePath = o => makePath(absolutePath, getImagePathFromRoot(o)),
4430			
4431		// Theme image path
4432		
4433		getThemeImagePath = o => makePath(getPath(o), settings.folderImageFile),
4434			
4435		// Original path
4436		
4437		getOriginalPath = o => (o.hasOwnProperty(J.ORIGINALFILE)?
4438				makePath(getPath(o), o[J.ORIGINALFILE]) 
4439				:
4440				(o.hasOwnProperty(J.ORIGINAL)? 
4441					makePath(getPath(o), o[J.ORIGINAL][J.PATH]) 
4442					: 
4443					null
4444				)
4445			),
4446			
4447		// Original custom page name (before renamed)
4448		
4449		getOriginalPageName = o => ((o[J.CATEGORY] === 'webPage' && o.hasOwnProperty(J.ORIGINALPAGENAME))?
4450				o[J.ORIGINALPAGENAME]
4451				:
4452				null
4453			),
4454			
4455		// Video path
4456		
4457		getVideoPath = o => (o.hasOwnProperty(J.VIDEO)?
4458				makePath(getPath(o), o[J.VIDEO][J.PATH]) 
4459				: 
4460				null
4461			),
4462			
4463		// Poster path for audio and video files
4464		
4465		getPosterPath = o => {
4466				let c = o[J.CATEGORY],
4467					ip = o[J.IMAGE][J.PATH];
4468				
4469				// Audio and video: using default poster graphics if iconpath was used
4470				if ((c === 'audio' && ip.endsWith('res/audio.png')) || (c === 'video' && ip.endsWith('res/movie.png'))) {
4471					return makePath(settings.rootPath, 'res', settings[c + 'Poster']);
4472				}
4473				
4474				return makePath(getPath(o), o[J.IMAGE][J.PATH]);
4475			},
4476			
4477		// Finding closest rendition
4478		
4479		getClosestRendition = (r, dim, fill) => {
4480				
4481				if (!WEBP_LOSSY) {
4482					// No webp support: filtering out
4483					r = r.filter(o => !o.name.endsWith('.webp'));
4484				}
4485				
4486				if (r.length > 1) {
4487					let w = dim[0] * PIXELRATIO,
4488						h = dim[1] * PIXELRATIO;
4489						
4490					// Variants: calculate scale ratio
4491					r.forEach(o => {
4492							let s = fill? Math.max(w / o.width, h / o.height) : Math.min(w / o.width, h / o.height);
4493							
4494							o.match = (s > 1)? 
4495									// Upscaling penalized by factor 3: 1x -> 120% == 2x -> 50%
4496									(3 * (1 - 1 / s))
4497									:
4498									// Downscaling
4499									(1 - s)
4500						});
4501						
4502					// Sort by scale
4503					r.sort((o1, o2) => o1.match - o2.match);
4504				}
4505				
4506				return r[0];
4507			},
4508			
4509		// Get path to optimum image. If no dim specified the image is optimized for the screen size.
4510		
4511		getOptimalImagePath = (o, dim, useOriginal, fill) => {
4512				var fill = (typeof fill === UNDEF)? false : fill;
4513				
4514				if (o.hasOwnProperty(J.LEVEL)) {
4515					// A folder
4516					return toRelPath(o) + '/' + settings.folderImageFile;
4517				}
4518				
4519				if (o[J.CATEGORY] === 'webLocation') {
4520					// Web location
4521					return getImagePath(o);
4522				}
4523			
4524				if (o[J.CATEGORY] === 'webPage') {
4525					// Web location
4526					return getThumbPath(o);
4527				}
4528			
4529				if (o.hasOwnProperty(J.ORIGINALFILE)) {
4530					// Non-image with original
4531					return getOriginalPath(o);
4532				}
4533			
4534				let r = o[J.IMAGE].hasOwnProperty(J.RENDITIONS)?
4535							getClosestRendition(o[J.IMAGE][J.RENDITIONS], dim || [ window.outerWidth, window.outerHeight ], fill)
4536							:
4537							(settings.hiDpiImages?
4538								{
4539									width:		2 * o[J.IMAGE][J.WIDTH],
4540									height:		2 * o[J.IMAGE][J.HEIGHT]
4541								}
4542								:
4543								{
4544									width:		o[J.IMAGE][J.WIDTH],
4545									height:		o[J.IMAGE][J.HEIGHT]
4546								}
4547							);
4548							
4549				if (typeof useOriginal !== UNDEF && useOriginal &&
4550					o.hasOwnProperty(J.ORIGINAL) && 
4551					(dim[0] > r.width || dim[1] > r.height) &&
4552					o[J.ORIGINAL][J.WIDTH] > o[J.IMAGE][J.WIDTH] &&
4553					isBrowserFriendly(o[J.ORIGINAL][J.PATH])) {
4554					// Using the original
4555					return getOriginalPath(o);
4556				}
4557				
4558				// No original: Using the closest rendition
4559				
4560				if (r.hasOwnProperty(J.NAME)) {
4561					// It's a HiDPI image
4562					return replaceFile(getImagePath(o), r[J.NAME]);
4563				}
4564				
4565				return getImagePath(o);
4566			},
4567		
4568		// Replace file name in a path
4569		
4570		replaceFile = (p, fn) => (p.slice(0, p.lastIndexOf('/') + 1) + fn),
4571		
4572		// Check if image dimensions are large enough to fit or fill the target
4573		
4574		hasEnoughPixels = (tdim, idim, mxz, fill) => (fill? 
4575				idim[0] * mxz >= tdim[0] && idim[1] * mxz >= tdim[1] 
4576				:
4577				idim[0] * mxz >= tdim[0] || idim[1] * mxz >= tdim[1]
4578			),
4579			
4580		// Get path to optimum thumb
4581		
4582		getOptimalThumbPath = (o, dim, useSlides) => {
4583			
4584				if (o[J.THUMB].hasOwnProperty(J.RENDITIONS)) {
4585					// Has renditions
4586					let r = getClosestRendition(o[J.THUMB][J.RENDITIONS], dim, settings.thumbsFillFrame);
4587					
4588					if (o[J.CATEGORY] === 'folder') {
4589						// Folder: only thumb path exists
4590						return makePath(getPath(o), replaceFile(o[J.THUMB][J.PATH], r[J.NAME]));
4591					} else if (o[J.CATEGORY] === 'video' || hasEnoughPixels(dim, [r[J.WIDTH], r[J.HEIGHT]], 1.07, settings.thumbsFillFrame)) {
4592						// Thumb is large enough: return thumb path
4593						return replaceFile(getThumbPath(o), r[J.NAME]);
4594					} else if (typeof useSlides !== UNDEF && useSlides && o[J.IMAGE].hasOwnProperty(J.RENDITIONS)) {
4595						// Try with image renditions
4596						r = getClosestRendition(o[J.IMAGE][J.RENDITIONS], dim, settings.thumbsFillFrame);
4597						return replaceFile(getImagePath(o), r[J.NAME]); 
4598					}
4599				}
4600				
4601				// No renditions
4602				let idim = [ o[J.THUMB][J.WIDTH], o[J.THUMB][J.HEIGHT] ];
4603					
4604				if (settings.hiDpiTHumbs) {
4605					idim[0] *= 2;
4606					idim[1] *= 2;
4607				}
4608				
4609				if (typeof useSlides !== UNDEF && useSlides && 
4610					(o[J.CATEGORY] !== 'video' && o.hasOwnProperty(J.IMAGE)) && 
4611					!hasEnoughPixels(dim, idim, 1.15, settings.thumbsFillFrame)) {
4612					// The thumb is too small, use the image
4613					return getImagePath(o);
4614				}
4615				
4616				// Returning the thumb
4617				return getThumbPath(o);
4618			},
4619		
4620		// Original or source path
4621		
4622		getSourcePath = (o) => makePath(getPath(o), 
4623				(o.hasOwnProperty(J.ORIGINAL)? 
4624					o[J.ORIGINAL][J.PATH] 
4625					: 
4626					(o.hasOwnProperty(J.IMAGE)? 
4627						o[J.IMAGE][J.PATH]
4628						:
4629						o[J.THUMB][J.PATH]
4630					)
4631				)
4632			),
4633			
4634		// Absolute path to an object as HTML page
4635		
4636		getAbsoluteItemPath = (o) => makePath(absolutePath, getItemPath(o)),
4637						
4638		// Get video duration in ms
4639		
4640		getVideoDuration = o => {
4641				let v = o[J.VIDEO];
4642				
4643				if (v && v.hasOwnProperty(J.DURATION)) {
4644					return videoDurationMs(v[J.DURATION]);
4645				}
4646				
4647				return null;
4648			},
4649			
4650		// Has shop options?
4651		
4652		hasShop = o => {
4653				if (typeof o === UNDEF) {
4654					// Should not happen, but returns true if the global shop option exists
4655					return tree.hasOwnProperty(J.SHOP) && tree.shop.hasOwnProperty('options') && tree.shop.options.trim() !== '-';
4656				}
4657				
4658				let shop = getInheritedPropertyObject(o, J.SHOP);
4659				
4660				return shop && (
4661							shop.hasOwnProperty('usePrice') && shop.usePrice && o.hasOwnProperty(J.PRICE) && o.price
4662							|| 
4663							shop.hasOwnProperty('options') && shop.options.trim() !== '-'
4664						);
4665			},
4666			
4667		// Has map location?
4668		
4669		hasLocation = o => (o.hasOwnProperty(J.HIDELOCATION) && o[J.HIDELOCATION]) ? 
4670				false
4671				:
4672				(	o.hasOwnProperty(J.LOCATION) ||
4673					o.hasOwnProperty(J.CAMERA) && o[J.CAMERA].hasOwnProperty(J.LOCATION)
4674				),
4675			
4676		// Get location in NN.NNN,NN.NNN format (Lat,long)
4677		
4678		getLocation = o => (o.hasOwnProperty(J.HIDELOCATION) && o[J.HIDELOCATION]) ?
4679				null
4680				:
4681				o.hasOwnProperty(J.LOCATION)? 
4682					o[J.LOCATION]
4683					:
4684					(
4685						(o.hasOwnProperty(J.CAMERA) && o[J.CAMERA].hasOwnProperty(J.LOCATION) && 
4686						 o[J.CAMERA][J.LOCATION].hasOwnProperty('lat') && o[J.CAMERA][J.LOCATION].hasOwnProperty('long')) ?
4687							(o[J.CAMERA][J.LOCATION]['lat'] + ',' + o[J.CAMERA][J.LOCATION]['long']) 
4688							:
4689							null
4690					),
4691				
4692		// Disable Feedback?
4693		
4694		disableFeedback = o => o.hasOwnProperty(J.DISABLEFEEDBACK)? 
4695				o[J.DISABLEFEEDBACK]
4696				:
4697				(getInheritedProperty(o, J.DISABLEFEEDBACK) || false)
4698			,		
4699			
4700		// Hide from Fotomoto?
4701		
4702		hideFotomoto = o => o.hasOwnProperty(J.HIDEFOTOMOTO)? 
4703				o[J.HIDEFOTOMOTO]
4704				:
4705				(getInheritedProperty(o, J.HIDEFOTOMOTO) || false)
4706			,		
4707			
4708		// Get the lowest price of an item
4709		
4710		getPriceRange = o => {
4711				let p = getInheritedPropertyObject(o || tree, J.SHOP);
4712				
4713				if (p && p['options'] !== '-' && p['showPriceRange']) { 
4714					let	opt = p.options.split('::'),
4715						min = Number.MAX_VALUE,
4716						max = Number.MIN_VALUE;
4717					
4718					if (opt.length > 1) {
4719						for (let i = 0; i < opt.length; i++) {
4720							min = Math.min(parseFloat(opt[i].split('=')[1].split('+')[0]), min);
4721						}
4722						if (p.showPriceRange === 'minmax') {
4723							for (let i = 0; i < opt.length; i++) {
4724								max = Math.max(parseFloat(opt[i].split('=')[1].split('+')[0]), max);
4725							}
4726							return toCurrency(min, p['currency']) + '&ndash;' + toCurrency(max, p['currency']);
4727						}
4728						return text.from.template(toCurrency(min, p['currency']));
4729					} else {
4730						return toCurrency(opt[0].split('=')[1].split('+')[0], p['currency']);
4731					}
4732				}
4733				
4734				return '';
4735			},
4736			
4737		// Get shopping cart's currency, falls back to EUR
4738			
4739		getCurrency = () => getRootProperty(J.SHOP)['currency'] || 'EUR',
4740			
4741		// Get custom page content (HTML)
4742		
4743		getCustomPageContent = (o) => {
4744				return (o && o.hasOwnProperty(J.CUSTOMPAGECONTENT))? o[J.CUSTOMPAGECONTENT] : '';
4745			},
4746			
4747		// Counting folders recursively (current folder included)
4748		
4749		getDeepFolderCount = f => {
4750				let c = 0,
4751					folder = (typeof f === UNDEF)? currentFolder : f;
4752				
4753				if (folder.hasOwnProperty(J.DEEPCOUNTERS) && folder[J.DEEPCOUNTERS].hasOwnProperty(J.FOLDERS)) {
4754					// Has deep folder count
4755					return folder[J.DEEPCOUNTERS][J.FOLDERS] + 1;
4756				}
4757				
4758				if (isDeep()) {
4759					if (folder.hasOwnProperty(J.OBJECTS)) {
4760						folder[J.OBJECTS].forEach(o => {
4761								if (o[J.CATEGORY] === 'folder' && !(o.hasOwnProperty(J.HIDDEN) && o[J.HIDDEN])) {
4762									c += getDeepFolderCount(o);
4763								}
4764							});
4765					}
4766				} else if (folder.hasOwnProperty(J.FOLDERS)) {
4767					folder[J.FOLDERS].forEach(o => {
4768							if (!(o.hasOwnProperty(J.HIDDEN) && o[J.HIDDEN])) {
4769								c += getDeepFolderCount(o);
4770							}
4771						});
4772				}
4773				
4774				// Adding missing deep counter
4775				if (!folder.hasOwnProperty(J.DEEPCOUNTERS)) {
4776					folder[J.DEEPCOUNTERS] = {};
4777				}
4778				
4779				// Saving deep folder count
4780				folder[J.DEEPCOUNTERS][J.FOLDERS] = c;
4781				
4782				return c + 1;
4783			},
4784			
4785		// Getting folder count with limited folder depth
4786			
4787		getFolderCount = (f, levels) => {
4788				let folder = (typeof f === UNDEF)? currentFolder : f,
4789					maxLevel = folder[J.LEVEL] + ((typeof levels === UNDEF)? 0 : levels),
4790					
4791					getCount = folder => {
4792							let c = 1;
4793							
4794							if (folder[J.LEVEL] <= maxLevel) {
4795								// Count only within max levels
4796								if (isDeep()) {
4797									if (folder.hasOwnProperty(J.OBJECTS)) {
4798										folder[J.OBJECTS].forEach(o => { 
4799												if (o[J.CATEGORY] === 'folder' && !(o.hasOwnProperty(J.HIDDEN) && o[J.HIDDEN])) {
4800													c += getCount(o);
4801												}
4802											});
4803									}
4804								} else if (folder.hasOwnProperty(J.FOLDERS)) { 
4805									folder[J.FOLDERS].forEach(o => { 
4806											if (!(o.hasOwnProperty(J.HIDDEN) && o[J.HIDDEN])) {
4807												c += getCount(o); 
4808											}
4809										});
4810								}
4811							}
4812							
4813							return c;
4814						};
4815						
4816				return getCount(folder);
4817			},
4818			
4819		/********************************************
4820		 *
4821		 *		  Generic property functions
4822		 *
4823		 */
4824		 
4825		// Returns a property from the root level
4826		
4827		getRootProperty = a => tree.hasOwnProperty(a)? tree[a] : null,
4828
4829		// Retrieving an Object property of an element with fallback to upper level folders
4830		
4831		getInheritedPropertyObject = (o, a) => {
4832				let p = {};
4833				
4834				do {
4835					if (o.hasOwnProperty(a)) {
4836						p = $.extend(true, {}, o[a], p);
4837					}
4838				} while (o = getParent(o));
4839				
4840				return (Object.getOwnPropertyNames(p)).length? p : null;
4841			},
4842						
4843		// Retrieving a single property of an element with fallback to upper level folders
4844		
4845		getInheritedProperty = (o, a) => {
4846				
4847				if (a.indexOf('.') >= 0) {
4848					a = a.split('.');
4849					
4850					if (a[0] === 'album') {
4851						return getRootProperty(a[1]);
4852					}
4853					
4854					do {
4855						if (o.hasOwnProperty(a[0])) {
4856							return $.extend(true, {}, o[a[0]][a[1]]);
4857						}
4858					} while (o = getParent(o));
4859					
4860					return null;
4861				}
4862					
4863				do {
4864					if (o.hasOwnProperty(a)) {
4865						return $.extend(true, {}, o[a]);
4866					}
4867				} while (o = getParent(o));
4868				
4869				return null;
4870			},
4871			
4872		// Returns a property (normal or inherited way)
4873		
4874		getProperty = (o, a, inherit) => {
4875				let r;
4876			
4877				if (inherit) {
4878					r = getInheritedProperty(o, a);
4879				} else if (a.indexOf('.') > 0) {
4880					a = a.split('.');
4881					r = (o.hasOwnProperty(a[0]))? o[a[0]][a[1]] : null;
4882				} else if (o.hasOwnProperty(a)) {
4883					r = o[a];
4884				}
4885				
4886				return $.extend(true, {}, r);
4887			},
4888		
4889		// Returns an Object property (normal or inherited way)
4890		
4891		getPropertyObject = (o, a, inherit) => ( 
4892				inherit? getInheritedPropertyObject(o, a) : (o.hasOwnProperty(a)? $.extend(true, {}, o[a]) : null)
4893			),
4894		
4895		// Getting folder's index in Folders array
4896		
4897		_getFolderIndex = (parent, f) => {
4898				let p = f[J.PATH],
4899					idx = -1;
4900					
4901				if (isDeep()) {
4902					if (parent.hasOwnProperty(J.OBJECTS)) {
4903						idx = parent[J.OBJECTS].findIndex(o => (o[J.CATEGORY] === 'folder' && o[J.PATH] === p));
4904					}
4905				} else {
4906					if (parent.hasOwnProperty(J.FOLDERS)) {
4907						idx = parent[J.FOLDERS].findIndex(f => f[J.PATH] === p);
4908					}
4909				}
4910				
4911				return idx;
4912			},	
4913					
4914		// Returns the next folder
4915		
4916		_getNextFolder = function(f) {
4917				let folder = f || currentFolder,
4918					parent = getParent(folder),
4919					idx = _getFolderIndex(parent, folder);
4920				
4921				if (idx >= 0) {
4922					if (settings.loadDeep) {
4923						if (parent.hasOwnProperty(J.OBJECTS)) {
4924							for (let i = idx + 1; i < parent[J.OBJECTS].length; i++) {
4925								if (parent[J.OBJECTS][i][J.CATEGORY] === 'folder' && 
4926									!(parent[J.OBJECTS][i].hasOwnProperty(J.HIDDEN) && parent[J.OBJECTS][i][J.HIDDEN])) {
4927									return parent[J.OBJECTS][i];
4928								}
4929							}
4930						}
4931					} else {
4932						if (parent.hasOwnProperty(J.FOLDERS)) {
4933							for (let i = idx + 1; i < parent[J.FOLDERS].length; i++) {
4934								if (!(parent[J.FOLDERS][i].hasOwnProperty(J.HIDDEN) && parent[J.FOLDERS][i][J.HIDDEN])) {
4935									return parent[J.FOLDERS][i];
4936								}
4937							}
4938						}
4939					}
4940				}
4941				
4942				return null;
4943			},
4944		
4945		// Returns the previous folder
4946		
4947		_getPreviousFolder = function(f) {
4948				let folder = f || currentFolder,
4949					parent = getParent(folder),
4950					idx = _getFolderIndex(parent, folder);
4951				
4952				if (idx >= 0) {
4953					if (settings.loadDeep) {
4954						if (parent.hasOwnProperty(J.OBJECTS)) {
4955							for (let i = idx - 1; i >= 0; i--) {
4956								if (parent[J.OBJECTS][i][J.CATEGORY] === 'folder' &&
4957									!(parent[J.OBJECTS][i].hasOwnProperty(J.HIDDEN) && parent[J.OBJECTS][i][J.HIDDEN])) {
4958									return parent[J.OBJECTS][i];
4959								}
4960							}
4961						}
4962					} else {
4963						if (parent.hasOwnProperty(J.FOLDERS)) {
4964							for (let i = idx - 1; i >= 0; i--) {
4965								if (!(parent[J.FOLDERS][i].hasOwnProperty(J.HIDDEN) && parent[J.FOLDERS][i][J.HIDDEN])) {
4966									return parent[J.FOLDERS][i];
4967								}
4968							}
4969						}
4970					}
4971				}
4972				
4973				return null;
4974			},
4975				
4976		// Get next folder's first image
4977		
4978		getNextFoldersFirstImage = function(readyFn) {
4979				let folder = _getNextFolder(currentFolder),
4980					
4981					_getFirstImage = function(folder) {
4982							if (folder.hasOwnProperty(J.OBJECTS)) {
4983								for (let i = 0; i < folder[J.OBJECTS].length; i++) {
4984									if (isLightboxable(folder[J.OBJECTS][i])) {
4985										return folder[J.OBJECTS][i];
4986									}
4987								}
4988							}
4989						};
4990				
4991				if (folder) {
4992					
4993					if (folder.hasOwnProperty(J.READY) && folder.ready) {
4994						readyFn(_getFirstImage(folder));
4995					} else {
4996						onFolderReady(folder, function(folder, args) {
4997								args[0](_getFirstImage(folder));
4998							}, [ readyFn ]);
4999					}
5000				}
5001			},
5002		
5003		// Get previous folder's last image
5004		
5005		getPreviousFoldersLastImage = function(readyFn) {
5006				let folder = _getPreviousFolder(currentFolder),
5007					
5008					_getLastImage = function(folder) {
5009							if (folder.hasOwnProperty(J.OBJECTS)) {
5010								for (let i = o.length - 1; i >= 0; i--) {
5011									if (isLightboxable(folder[J.OBJECTS][i])) {
5012										return folder[J.OBJECTS][i];
5013									}
5014								}
5015							}
5016						};
5017			
5018				
5019				if (folder) {
5020					
5021					if (folder.hasOwnProperty(J.READY) && folder.ready) {
5022						readyFn(_getLastImage(folder));
5023					} else {
5024						onFolderReady(folder, function(folder, args) {
5025								args[0](_getLastImage(folder));
5026							}, [ readyFn ]);
5027					}
5028				}
5029				
5030				return null;
5031			},
5032			
5033		/***************************************************************
5034		 *
5035		 *	Sort items: images, folders
5036		 *		sortBy:			sort criteria ('original' | 'random' | 'date' | 'name' | 'fileSize')
5037		 *		reference:		if sort by date ('added' | 'fileModified' | 'dateTaken')
5038		 *		reverse:		false = normal, true = reversed
5039		 *		foldersFirst:	put folders first
5040		 */
5041		
5042		sortItems = function(images, folders, opt) {
5043			
5044				if (!Array.isArray(images)) {
5045					// Images array is mandatory
5046					return null;
5047				}
5048				
5049				if (!Array.isArray(folders)) {
5050					// No folders provided
5051					opt = folders || {};
5052					folders = null;
5053				}
5054				
5055				let options = $.extend({
5056							sortBy:				'original',			// No change
5057							reference:			J.DATETAKEN,		// Taken date
5058							reverse:			false,				// Not reversed
5059							foldersFirst:		true				// Put folders first
5060						}, opt);
5061				
5062				// Ordering items
5063				switch (options.sortBy) {
5064					
5065					case 'random':
5066						
5067						// Random
5068						
5069						images.sort(() => (0.5 - Math.random()));
5070						
5071						if (folders) {
5072							folders.sort(() => (0.5 - Math.random()));
5073						}
5074						
5075						break;
5076					
5077					case 'date': 
5078						
5079						// Date
5080						
5081						let ref = options['reference'];
5082						
5083						if (options.reverse) {
5084							
5085							images.sort((a, b) => (
5086									(a.hasOwnProperty(J.DATES)? a[J.DATES][ref] : a[J.FILEDATE]) - 
5087									(b.hasOwnProperty(J.DATES)? b[J.DATES][ref] : b[J.FILEDATE])
5088								));
5089							
5090							if (folders) {
5091								folders.sort((a, b) => (a[J.FILEDATE] - b[J.FILEDATE]));
5092							}
5093							
5094						} else {
5095							
5096							images.sort((a, b) => (
5097									(b.hasOwnProperty(J.DATES)? b[J.DATES][ref] : b[J.FILEDATE]) - 
5098									(a.hasOwnProperty(J.DATES)? a[J.DATES][ref] : a[J.FILEDATE])
5099								));
5100							
5101							if (folders) {
5102								folders.sort((a, b) => (b[J.FILEDATE] - a[J.FILEDATE]));
5103							}
5104						}
5105						
5106						break;
5107						
5108					case J.NAME:
5109						
5110						// File name
5111						
5112						if (!options.reverse) {
5113							
5114							images.sort((a, b) => ('' + a[J.NAME]).localeCompare('' + b[J.NAME]));
5115							
5116							if (folders) {
5117								folders.sort((a, b) => ('' + a[J.NAME]).localeCompare('' + b[J.NAME])); 
5118							}
5119							
5120						} else {
5121							
5122							images.sort((a, b) => ('' + b[J.NAME]).localeCompare('' + a[J.NAME]));
5123							
5124							if (folders) {
5125								folders.sort((a, b) => ('' + b[J.NAME]).localeCompare('' + a[J.NAME]));
5126							}
5127							
5128						}
5129						
5130						break;
5131						
5132					case J.FILESIZE:
5133						
5134						// File size: only images
5135						
5136						if (options.reverse) {
5137							images.sort((a, b) => (a[J.FILESIZE] - b[J.FILESIZE]));
5138						} else {
5139							images.sort((a, b) => (b[J.FILESIZE] - a[J.FILESIZE]));
5140						}
5141						
5142						break;
5143						
5144					default:
5145						
5146						// Descending?
5147						if (options.reverse) {
5148							
5149							images.reverse();
5150							
5151							if (folders) {
5152								folders.reverse();
5153							}
5154						}
5155						
5156						break;
5157						
5158				}
5159				
5160				// Concatenating folders and images arrays
5161				
5162				if (folders) {
5163					if (options.foldersFirst) {
5164						return folders.concat(images);
5165					} else {
5166						return images.concat(folders);
5167					}
5168				}
5169				
5170				return images;
5171			
5172			},
5173
5174		/***************************************************************
5175		 *
5176		 *	collectByPath: Collect items by their paths within the album structure
5177		 *		paths:			array of root paths
5178		 *		sortBy:			sort criteria ('original' | 'random' | 'date' | 'name' | 'fileSize')
5179		 *		reference:		if sort by date ('added' | 'fileModified' | 'dateTaken')
5180		 *		reverse:		false = normal, true = reversed
5181		 *		ready:			the function to call after items ready
5182		 */
5183		
5184		collectByPath = function(opt) {
5185				
5186				//log('collectByPath(' + JSON.stringify(opt) + ')');
5187				if (typeof opt === UNDEF || !opt.hasOwnProperty('paths') || !Array.isArray(opt.paths) || typeof opt['ready'] !== FUNCTION) {
5188					return [];
5189				}
5190				
5191				let	items 			= [],
5192					options 		= $.extend({
5193												folder:			'',				// Root folder
5194												levels:			0,				// Current folder only
5195												sortBy:			'original',		// No sort
5196												reference:		'dateTaken',	// Reference
5197												reverse:		false			// Ascending
5198											}, opt),
5199					max				= options.paths.length,
5200					counter			= 0,
5201					fto,
5202					folder			= getFolder(options.folder),
5203					
5204					_skipEmpty = function(items) {
5205							let arr = [];
5206							
5207							for (let i of items) {
5208								i && arr.push(i);
5209							}
5210							
5211							return arr;
5212						},
5213					
5214					_finished = function() {
5215						
5216							clearTimeout(fto);
5217							
5218							if (counter < max) {
5219								log('Error: Timeout collecting items. Image set is incomplete (' + counter + '/' + max + ')!');
5220								items = _skipEmpty(items)
5221							}
5222							
5223							items = sortItems(items, {
5224									sortBy:			options['sortBy'],
5225									reference:		options['reference'],
5226									reverse:		options['reverse']
5227								});
5228							
5229							options.ready.call(items, options);
5230						};
5231				
5232				// Allowing 25ms / item to collect, so it won't run infinitely if some items missing
5233				fto = setTimeout(_finished, max * 25);
5234				
5235				for (let n = 0; n < max; n++) {
5236					
5237					getItem(options.paths[n], function(i) {
5238							// Non-linear
5239							if (this && typeof i !== UNDEF && this !== window) {
5240								items[i] = this;
5241								if (++counter === max) {
5242									// All items got collected
5243									clearTimeout(fto);
5244									_finished();
5245								}
5246							}
5247						}, n);
5248				}
5249				
5250				
5251			},
5252			
5253		/***************************************************************
5254		 *
5255		 *	collectNItem: Collect items from folder
5256		 *		folder:			start folder
5257		 *		levels:			depth below the start folder
5258		 *		max: 			maximum number
5259		 *		arRange:		[ min, max ] - aspect ratio range
5260		 *		sortBy:			sort criteria ('original' | 'random' | 'date' | 'name' | 'fileSize')
5261		 *		reference:		if sort by date ('added' | 'fileModified' | 'dateTaken')
5262		 *		sortOrder:		1: ascending 0: descending
5263		 *		quick:			true: stops when enough element has been gathered 
5264		 *						false: loads all folders before it selects the "max" elements based on sort
5265		 */
5266		
5267		collectNItem = function(opt) {
5268				
5269				//log('collectNItem(' + JSON.stringify(opt) + ')');
5270				if (typeof opt === UNDEF || typeof opt['ready'] !== FUNCTION) {
5271					return;
5272				}
5273				
5274				let options 		= $.extend({
5275											folder:			'',				// Root folder
5276											levels:			0,				// Current folder only
5277											include:		'images',		// Items to include
5278											max:			0,				// No limit
5279											sortBy:			'original',		// No sort			
5280											sortOrder:		0				// Ascending
5281										}, opt),
5282					items 			= [],
5283					folders			= [],
5284					folder			= getFolder(options.folder),
5285					foldersToLoad 	= options.levels? getFolderCount(folder, options.levels) : 1,
5286					foldersLoaded	= 0,
5287					completed		= false,
5288					needsDeepData	= false, //options.levels > 0 && folder.hasOwnProperty(J.FOLDERS),
5289					useImages		= options.include.indexOf('images') !== -1,
5290					useFolders		= options.include.indexOf('folders') !== -1,
5291				
5292					_addItems = function(folder) {
5293							// Adding images in a folder
5294							folder[J.OBJECTS].forEach(o => {
5295									if (o[J.CATEGORY] === 'image' && 
5296											(
5297												!options['arRange'] || 
5298												(o[J.IMAGE][J.WIDTH] / o[J.IMAGE][J.HEIGHT] >= options.arRange[0] &&
5299												o[J.IMAGE][J.WIDTH] / o[J.IMAGE][J.HEIGHT] <= options.arRange[1])
5300											)
5301										) {
5302										items.push(o);
5303									}
5304								});
5305						},
5306					
5307					_addFolder = function(folder) {
5308						
5309							if (useFolders) {
5310								folders.push(folder);
5311							}
5312							
5313							if (useImages) {
5314								// Adding objects of one folder
5315								if (folder.hasOwnProperty(J.OBJECTS)) {
5316									// Already loaded
5317									_addItems(folder);
5318								} else if (!folder[J.READY]) {
5319									// Wait to be loaded
5320									onFolderReady(folder, _addItems);
5321									//loadFolder(folder);
5322								}
5323							}
5324							
5325							if (folder[J.LEVEL] <= maxLevel) {
5326								// recursive to subfolders
5327								if (isDeep()) {
5328									if (folder.hasOwnProperty(J.OBJECTS)) {
5329										folder[J.OBJECTS].forEach(o => {
5330												if (o[J.CATEGORY] === 'folder' && !(o.hasOwnProperty(J.HIDDEN) && o[J.HIDDEN]) && !o.hasOwnProperty(J.VIEW)) {
5331													_addFolder(o);
5332												}
5333											});
5334									}
5335								} else if (folder.hasOwnProperty(J.FOLDERS)) {
5336									folder[J.FOLDERS].forEach(o => {
5337											if (!(o.hasOwnProperty(J.HIDDEN) && o[J.HIDDEN]) && !o.hasOwnProperty(J.VIEW)) {
5338												_addFolder(o);
5339											}
5340										});
5341								}
5342							}
5343							
5344							foldersLoaded++;
5345						},
5346						
5347					// Using deep-data	
5348					_readyDeep = function() {
5349						
5350							_addFolder(folder);
5351							
5352							setTimeout(_finished, 20);
5353						},
5354						
5355					// Loading data1.json files one-by-one
5356					_loadAll = function() {
5357						
5358							// Deep data already loaded or data1.json is enough
5359							_addFolder(folder);
5360							
5361							// Allow some time to add the first defer
5362							setTimeout(function() {
5363									if (defer.length) {
5364										$.when.apply($, defer).done(_finished);
5365									} else {
5366										_finished();
5367									}
5368								}, 20);						
5369						},
5370						
5371					_finished = function() {
5372						
5373							if (completed) {
5374								return;
5375							}
5376							
5377							if (options.max && options.quick && (items.length + folders.length) >= options.max ||  
5378								!defer.length || (foldersLoaded >= foldersToLoad)) { 
5379							
5380								items = sortItems(items, folders, {
5381										sortBy:			(options['sortOrder'] === -1)? 'random' : (options['sortBy'] || 'original'),
5382										reference:		options['reference'] || J.FILEDATE,
5383										reverse:		options['sortOrder'] === 0,
5384										foldersFirst:	options['include'].startsWith('folders')
5385									});
5386									
5387								// Chopping the extra items
5388								if (options.max && options.max < items.length) {
5389									items = items.slice(0, options.max);
5390								}
5391								
5392								completed = true;
5393								options.ready.call(items, options);
5394								
5395							} else {
5396								setTimeout(_finished, 50);
5397								return;
5398							}
5399						};
5400						
5401				// Starting a new promise collect
5402				defer = [];
5403				completed = false;
5404				
5405				if (!options.hasOwnProperty('quick')) {
5406					options.quick = options.max && options.sortBy !== 'original';
5407				}
5408				
5409				maxLevel = folder[J.LEVEL] + options.levels;
5410				
5411				random = options.sortOrder === -1;
5412				if (random) {
5413					settings.sortBy = 'original';
5414				}
5415					
5416				if (needsDeepData && !deepReady) {
5417					// Loading deep data, falling back to recursive data1.json on error
5418					loadDeep(_readyDeep, _loadAll);
5419					
5420				} else {
5421					// No deep data needed, loading current and its subtree (if needed)
5422					_loadAll();
5423				}
5424			},
5425			
5426		/***************************************************************
5427		 *
5428		 *	collectByDate: Collect items by date
5429		 *		depth:			where to collect ('tree' | 'current' | 'subfolders')
5430		 *		sortBy:			sort criteria ('original' | 'random' | 'date' | 'name' | 'fileSize')
5431		 *		reference:		if sort by date ('added' | 'fileModified' | 'dateTaken')
5432		 *		reverse:		false = normal, true = reversed
5433		 *		range:			time range
5434		 *							range = now - range ... now
5435		 *							range, start = start ... start + range
5436		 *							range, end = end - range ... end
5437		 *							Where start and end are days since 1900-01-01
5438		 *		Returns only lightboxable items
5439		 */
5440		
5441		collectByDate = function(opt) {
5442			
5443				//log('collectByDate(' + JSON.stringify(opt) + ')');
5444				if (typeof opt === UNDEF || typeof opt['ready'] !== FUNCTION) {
5445					return;
5446				}
5447				
5448				let options 		= $.extend({
5449											sort:			true,
5450											reverse:		false,
5451											reference:		J.DATETAKEN,
5452											depth: 			'current' 		// 'tree' | 'current' | 'subfolders'
5453										}, opt),
5454					items 			= [],
5455					start,
5456					end, 
5457					foldersToLoad 	= (options.depth === 'current')? 1 : getDeepFolderCount((options.depth === 'tree')? tree : currentFolder),
5458					foldersLoaded	= 0,
5459					completed		= false,
5460					needsDeepData	= options.depth === 'tree' && tree.hasOwnProperty(J.FOL
5460DERS) ||
5461									  options.depth === 'subfolders' && currentFolder.hasOwnProperty(J.FOLDERS) && currentFolder[J.LEVEL] < 3,
5462				
5463					
5464					_findByDate = function(folder) {
5465							
5466							// Find images that fall into the date range
5467							
5468							if (!folder || !folder.hasOwnProperty(J.OBJECTS)) {
5469								return;
5470							}
5471							
5472							folder[J.OBJECTS].forEach(o => {
5473									if (isLightboxable(o) &&
5474										(d = o[J.DATES]) && 
5475										(d = d[options.reference]) && 
5476										(d >= start) && (d <= end)) {
5477										items.push(o);
5478									}
5479								});
5480														
5481							foldersLoaded++;
5482						},
5483					
5484					_addFolder = function(folder) {
5485							// Adding one folder
5486							
5487							if (!folder || (folder.hasOwnProperty(J.HIDDEN) && folder.hidden)) {
5488								return;
5489							}
5490							
5491							// Adding objects of one folder
5492							if (folder.hasOwnProperty(J.OBJECTS)) {
5493								// Already loaded
5494								_findByDate(folder);
5495							} else if (!folder[J.READY]) {
5496								// Wait to be loaded
5497								onFolderReady(folder, _findByDate);
5498								//loadFolder(folder);
5499							}
5500							
5501							if (isDeep()) {
5502								if (folder.hasOwnProperty(J.OBJECTS)) {
5503									folder[J.OBJECTS].forEach(o => {
5504											if (o[J.CATEGORY] === 'folder' && !(o.hasOwnProperty(J.HIDDEN) && o[J.HIDDEN]) && !o.hasOwnProperty(J.VIEW)) {
5505												_addFolder(o);
5506											}
5507										});
5508								}
5509							} else if (folder.hasOwnProperty(J.FOLDERS)) {
5510								folder[J.FOLDERS].forEach(o => {
5511										if (!(o.hasOwnProperty(J.HIDDEN) && o[J.HIDDEN]) && !o.hasOwnProperty(J.VIEW)) {
5512											_addFolder(o);
5513										}
5514									});
5515							}
5516						},
5517						
5518					_readyDeep = function() {
5519						
5520							_addFolder((options.depth === 'tree')? tree : currentFolder);
5521							
5522							setTimeout(_finished, 20);
5523						},
5524						
5525					_loadAll = function() {
5526						
5527							// Deep data already loaded or data1.json is enough
5528							_addFolder((options.depth === 'tree')? tree : currentFolder);
5529							
5530							// Allow some time to add the first defer
5531							setTimeout(function() {
5532									if (defer.length) {
5533										$.when.apply($, defer).done(_finished);
5534									} else {
5535										_finished();
5536									}
5537								}, 20);						
5538						},
5539						
5540					_finished = function() {
5541						
5542							if (completed) {
5543								return;
5544							}
5545							
5546							//console.log('_finished: toLoad=' + foldersToLoad + ' loaded=' + foldersLoaded);
5547							if (defer.length && foldersToLoad > foldersLoaded) {
5548								setTimeout(_finished, 20);
5549								return;
5550							}
5551							
5552							// Ordering items
5553							
5554							if (options.sort) {
5555								items = sortItems(items, {
5556									sortBy:			options['sortBy'] || 'date',
5557									reference:		options['reference'] || J.DATETAKEN,
5558									reverse:		options['reverse'] || false
5559								});
5560							}
5561							
5562							if (options.max && options.max < items.length) {
5563								items = items.slice(0, options.max);
5564							}
5565							
5566							completed = true;
5567							options.ready.call(items, options);
5568						};
5569				
5570				// Starting a new promise collect
5571				defer = [];
5572				completed = false;
5573				
5574				// options.start and options.end are days since 1970-01-01, range is number of days
5575				if (options.hasOwnProperty('end')) {
5576					end = options.end * ONEDAY_S;
5577				}
5578				
5579				if (options.hasOwnProperty('start')) {
5580					start = options.start * ONEDAY_S;
5581				}
5582				
5583				if (options.hasOwnProperty('range')) {
5584					if (typeof start !== UNDEF) {
5585						end = start + options.range * ONEDAY_S;
5586					} else if (typeof end !== UNDEF) {
5587						start = end - options.range * ONEDAY_S;
5588					} else {
5589						// Up to now
5590						end = Math.round(new Date() / 1000);
5591						start = end - options.range * ONEDAY_S;
5592					}
5593				}
5594				
5595				if (typeof start === UNDEF) {
5596					start = 0;
5597				}
5598				
5599				if (typeof end === UNDEF) {
5600					end = Math.round(new Date() / 1000);
5601				}
5602								
5603				if (needsDeepData && !deepReady) {
5604					
5605					// Loading deep data, falling back to recursive data1.json on error
5606					loadDeep(_readyDeep, _loadAll);
5607					
5608				} else {
5609					
5610					_loadAll();
5611				}
5612			},
5613		
5614		/***************************************************************
5615		 *
5616		 *	collectItems: Collecting search results
5617		 *
5618		 *		fields: 		fields to watch
5619		 *		types:			'all' or comma separated list of allowed types ('image' | 'audio' | 'video' | ...)
5620		 *		depth:			where to collect ('tree' | 'current' | 'subfolders')
5621		 *		exact:			exact search (or conjunctive)
5622		 *  	caseSensitive:	case sensitivity: (false by default)
5623		 *		max:			maximum number of results
5624		 */
5625		
5626		collectItems = function(opt) {
5627			
5628				//log('collectItems(' + JSON.stringify(opt) + ')');
5629				if (typeof opt === UNDEF || !opt.hasOwnProperty('terms') || typeof opt['ready'] !== FUNCTION) {
5630					return;
5631				}
5632				
5633				let options 		= $.extend({
5634												fields: 		'creator,keywords,name,title,comment,regions',
5635												types:			'all',
5636												depth: 			'current', 		// 'tree' | 'current' | 'subfolders'
5637												divisible:		'name,title,comment,photodata',
5638												exact: 			false,
5639												caseSensitive:	false	
5640											}, opt),
5641					items 			= [], 																	// Found items
5642					terms,													// Terms to look for - used for undivisible fields
5643					termsArr,																				// Terms to look for (Array) - used for divisible fields
5644					fields 			= Array.isArray(options.fields)? options.fields : options.fields.split(/,\s?/), // fields to check
5645					fieldslength 	= fields.length,														// Fields length
5646					divisible		= new Array(fieldslength),												// Fields that can be divided
5647					exact			= false,																// Exact search: ""foo boo""
5648					conjunctive 	= false,																// Conjunctive search: "foo and boo"
5649					splitRegExp,																			// Split RegExp
5650					hasSeparator,																			// Search term contains "-", ".", "/" or "_"
5651					allTypes		= options.types === 'all',												// Every item type to check
5652					types 			= {},																	// Item types to check
5653					foldersToLoad 	= (options.depth === 'current')? 
5654											1 
5655											: 
5656											getDeepFolderCount((options.depth === 'tree')? tree : currentFolder),
5657					foldersLoaded	= 0,
5658					completed		= false,
5659					needsDeepData	= options.depth === 'tree' && tree.hasOwnProperty(J.FOLDERS) ||
5660									  options.depth === 'subfolders' && currentFolder.hasOwnProperty(J.FOLDERS) && currentFolder[J.LEVEL] < 3,
5661										
5662					_searchItem = function(o, cat) {
5663						
5664							let found = new Array(termsArr.length).fill(false),
5665								
5666								foundAny = () => found.some(el => !!el),
5667								
5668								foundAll = () => found.every(el => !!el),
5669									
5670								//hasFound = () => conjunctive? allTrue() : anyTrue(),
5671								
5672								// Finds 
5673								find = function(term, s, div) {
5674										
5675										s = s.replaceAll('"', '');
5676										
5677										if (!options.caseSensitive) {
5678											s = s.toLowerCase();
5679										}
5680										
5681										if (div) {
5682											// Divisible field
5683											if (!hasSeparator) {
5684												s = s.replace(splitRegExp, ' ');
5685											}
5686											return (' ' + s).includes(' ' + term);
5687										}
5688										
5689										if (exact) {
5690											// Exact search
5691											return term === s;
5692										}	
5693										
5694										// Not exact but must start with the search string
5695										return s.startsWith(term);
5696									},
5697									
5698								// Search for a single term
5699								searchTerm = function(term, s, div) {
5700									
5701										if (!Array.isArray(s)) {
5702											// The value is single
5703											if (exact) {
5704												s = s.toLowerCase();
5705												if (div) {
5706													return s.slice(0, term.length) === term || s.indexOf(' ' + term) !== -1;
5707												}
5708												return s === term;
5709											}
5710											
5711											return find(term, s, div);
5712										}
5713										
5714										// The value is an Array
5715										let m = false;
5716										
5717										for (let i = 0; i < s.length; i++) {
5718											if (find(term, s[i], div)) {
5719												m = true;
5720											}
5721										}
5722										
5723										return m;
5724									},
5725								
5726								// Search for terms: single or multiple terms 
5727								searchTerms = function(terms, s, div) {
5728										let m = false;
5729									
5730										if (exact && s.includes(';')) {
5731											// Treat like an array
5732											s = s.split(';');
5733										}
5734										
5735										if (Array.isArray(terms)) {
5736											// Multi term search
5737											for (let i = 0; i < terms.length; i++) {
5738												if (searchTerm(terms[i], s, div)) {
5739													m = found[i] = true;
5740													if (!conjunctive) {
5741														return true;
5742													}
5743												}
5744											}
5745											//log(o[J.NAME] + ' searchTerms("' + terms + '", "' + s + '", ' + exact + ') ==> ' + m);
5746										} else {
5747											// One term search
5748											if (searchTerm(terms, s, div)) {
5749												m = found[0] = true;
5750											}
5751										}
5752										
5753										return m; 
5754									};
5755								
5756							for (let i = 0, f, p; i < fieldslength; i++) {
5757								
5758								// Category specific field?
5759								if (fields[i].length > 1) {
5760									// e.g. "folder:title"
5761									if (fields[i][0] !== cat) {
5762										// Skip this type
5763										continue;
5764									}
5765									f = fields[i][1];
5766								} else {
5767									f = fields[i][0];
5768								}
5769								
5770								if (JCAMERAFIELDS.indexOf(f) >= 0 && o.hasOwnProperty(J.CAMERA)) {
5771									// Camera data
5772									p = o[J.CAMERA][f];
5773									if (typeof p === UNDEF) {
5774										// not found: retry on plain attribute, e.g. "aperture"
5775										p = o[f];
5776									}
5777									
5778								} else if (f === J.NAME) {
5779									// File name
5780									if (o.hasOwnProperty(J.ORIGINAL)) {
5781										// Has original: use that's file name
5782										p = decodeURIComponent(o[J.ORIGINAL][J.PATH].getFile());
5783									} else {
5784										// Slide image name
5785										p = o[J.NAME];
5786									}
5787									// Searching for the whole name and parts broken by word boundaries 
5788									p = p + ' ' + p.replace(/[\.\-_]/g, ' ');
5789									
5790								} else if (f === J.REGIONS) {
5791									// Regions
5792									if (o.hasOwnProperty(J.REGIONS)) {
5793										// Get names
5794										p = [];
5795										JSON.parse(o[J.REGIONS]).forEach(a => p.push(a.split(';')[0] + ''));
5796										p = p.filter(Boolean);
5797									} else {
5798										p = null;
5799									}
5800								} else {
5801									// All other fields
5802									p = o[f];
5803								}
5804									
5805								// Has such a property?
5806								if (typeof p !== UNDEF && p !== null && p.length) {
5807									if (Array.isArray(p) && !divisible[i]) {
5808										// The property is an Array and undivisible
5809										for (let j = 0; j < p.length; j++) {
5810											if (searchTerms((conjunctive || !exact)? termsArr : terms, p[j], divisible[i])) {
5811												if (!conjunctive) {
5812													// Not conjunctive: return on the first found
5813													return true;
5814												}
5815											}
5816										}
5817										
5818									} else {
5819										// Not exact or not array
5820										if (f === J.COMMENT || f.endsWith('Caption')) {
5821											// Strip HTML on fields might contain it
5822											p = p.stripHTML();
5823										} else if (Array.isArray(p)) {
5824											// Array
5825											p = p.join(' ');
5826										} else {
5827											// Otherwise ensure it's a string
5828											p = p + '';
5829										}
5830										
5831										if (searchTerms((conjunctive || !exact)? termsArr : terms, p, divisible[i])) {
5832											if (!conjunctive) {
5833												// Found one: don't care about other fields
5834												return true;
5835											}
5836										}
5837									}
5838								}
5839							}
5840							
5841							// all terms found (conjunctive) | any term found otherwise
5842							return conjunctive? foundAll() : foundAny();
5843						},
5844						
5845					_searchFolder = function(folder) {
5846							/*
5847							if (typeof DEBUG !== UNDEF && DEBUG) {
5848								log('Searching folder "' + folder[J.NAME] + '" ' + (folder[J.OBJECTS]? folder[J.OBJECTS].length : 0) + ' items');
5849							}
5850							*/
5851							if (!folder) {
5852								return;
5853							}
5854							if (folder.hasOwnProperty(J.OBJECTS)) {
5855								// Objects
5856								folder[J.OBJECTS].forEach(o => {
5857										if (o.hasOwnProperty(J.FOLDERINDEX)) {
5858											// it's just a reference to Folders[] (when using data1.json)
5859											if (allTypes || types['folder']) {
5860												if (_searchItem(folder[J.FOLDERS][o[J.FOLDERINDEX]], 'folder')) {
5861													items.push(o);
5862												}
5863											}
5864										} else {
5865											// Normal tree
5866											if (o.hasOwnProperty(J.CATEGORY) && (allTypes || types[o[J.CATEGORY]])) {
5867												if (_searchItem(o, o[J.CATEGORY])) {
5868													items.push(o);
5869												}
5870											}
5871										}
5872									});
5873							}
5874							
5875							foldersLoaded++;
5876						},
5877				
5878					_addFolder = function(folder) {
5879							// Adding one folder
5880							
5881							if (!folder || (folder.hasOwnProperty(J.HIDDEN) && folder.hidden)) {
5882								return;
5883							}
5884							
5885							// Adding objects of one folder
5886							if (folder.hasOwnProperty(J.OBJECTS)) {
5887								// Already loaded
5888								_searchFolder(folder);
5889							} else if (!folder[J.READY]) {
5890								// Wait to be loaded
5891								onFolderReady(folder, _searchFolder);
5892								//loadFolder(folder);
5893							}
5894							
5895							if (isDeep()) {
5896								if (folder.hasOwnProperty(J.OBJECTS)) {
5897									folder[J.OBJECTS].forEach(o => {
5898											if (o[J.CATEGORY] === 'folder' && !(o.hasOwnProperty(J.HIDDEN) && o[J.HIDDEN]) && !o.hasOwnProperty(J.VIEW)) {
5899												_addFolder(o);
5900											}
5901										});
5902								}
5903							} else if (options.depth !== 'current' && folder.hasOwnProperty(J.FOLDERS)) {
5904								folder[J.FOLDERS].forEach(o => {
5905										if (!(o.hasOwnProperty(J.HIDDEN) && o[J.HIDDEN]) && !o.hasOwnProperty(J.VIEW)) {
5906											_addFolder(o);
5907										}
5908									});
5909							}
5910						},
5911						
5912					_readyDeep = function() {
5913						
5914							_addFolder((options.depth === 'tree')? tree : currentFolder);
5915							setTimeout(_finished, 20);
5916						},
5917						
5918					_loadAll = function() {
5919						
5920							// Deep data already loaded or data1.json is enough
5921							_addFolder((options.depth === 'tree')? tree : currentFolder);
5922							
5923							// Allow some time to add the first defer
5924							setTimeout(function() {
5925									if (defer.length) {
5926										$.when.apply($, defer).done(_finished);
5927									} else {
5928										_finished();
5929									}
5930								}, 20);						
5931						},
5932						
5933					_finished = function() {
5934							
5935							if (completed) {
5936								return;
5937							}
5938							
5939							//console.log('_finished: toLoad=' + foldersToLoad + ' loaded=' + foldersLoaded);
5940							if (defer.length && foldersToLoad > foldersLoaded) {
5941								// Premature call for finished
5942								setTimeout(_finished, 20);
5943								return;
5944							}
5945							
5946							if (options.max && options.max < items.length) {
5947								items = items.slice(0, options.max);
5948							}
5949							
5950							completed = true;
5951							options.ready.call(items, options);
5952						};
5953				
5954				// Starting a new promise collect
5955				defer = [];
5956				completed = false;
5957				terms = options.terms.trim();
5958
5959				if (!options.caseSensitive) {
5960					terms = terms.toLowerCase();
5961				}
5962				
5963				if (options.terms[0] === '"' && options.terms[options.terms.length - 1] === '"') {
5964					// Exact search with quotes: "something exact"
5965					terms = terms.slice(1, -1).trim();
5966					termsArr = [ terms ];
5967					exact = !opt.hasOwnProperty('exact')? true : opt.exact;
5968					
5969				} else {
5970					// Any word or conjunctive
5971					let lct = terms.toLowerCase();
5972						
5973					if (andRegExp.test(lct)) {
5974						// Conjunctive search
5975						termsArr = terms.split(andRegExp);
5976						terms = terms.replace(andRegExp, ' ');
5977						conjunctive = true;
5978					} else {
5979						// Normal search
5980						termsArr = terms.split(' ');
5981						conjunctive = false;	
5982					}
5983					// Removing empty elements
5984					termsArr = termsArr.filter(Boolean);
5985					splitRegExp	= /^\d[\d\-\/\.\:]+\d?$/.test(terms) ? 			// RegExp that works as word delimiter
5986									/[,_|\n\t_]+/g : 							// Search string is date-like
5987									/[.,_\-|:/\n\t]+/g;							// Search string is not date
5988					exact = opt.hasOwnProperty('exact')? opt.exact : false;
5989				}
5990				
5991				hasSeparator = /[-._/]/.test(terms);
5992				
5993				// Determining divisible fields
5994				for (let i = 0, f, d = options.divisible.split(','); i < fieldslength; i++) {
5995					fields[i] = fields[i].split(':');
5996					divisible[i] = d.indexOf(fields[i][1] || fields[i][0]) >= 0;
5997				}
5998				
5999				if (!allTypes) {
6000					if (options.types.charAt(0) === '-') {
6001						// Negative
6002						settings.possibleTypes.forEach(t => {
6003								if (options.types.indexOf(t) === -1) {
6004									types[t] = true;
6005								}
6006							});
6007					} else {
6008						// Positive
6009						options.types.split(/,\s?/).forEach(t => { types[t] = true; });
6010					}
6011				}
6012				
6013				if (needsDeepData) {
6014					
6015					if (!deepReady) {
6016						// Loading deep data, falling back to recursive data1.json on error
6017						loadDeep(_readyDeep, _loadAll);
6018					} else {
6019						_readyDeep();
6020					}
6021					
6022				} else {
6023					
6024					_loadAll();
6025				}
6026									
6027			},
6028		
6029		/***************************************************************
6030		 *
6031		 *	collectTags:	Collecting tags for Tag cloud
6032		 *		fields: 		fields to watch
6033		 *		types:			'all' or comma separated list of allowed types ('image' | 'audio' | 'video' | ...)
6034		 *		depth:			where to collect ('tree' | 'current' | 'subfolders')
6035		 *		divisible:		those fields that can be splitted around spaces or dashes
6036		 */
6037		
6038		collectTags = function(opt) {
6039			
6040				//log('collectTags(' + JSON.stringify(opt) + ')');
6041				if (typeof opt === UNDEF || typeof opt['ready'] !== FUNCTION) {
6042					return;
6043				}
6044					
6045				let options 		= $.extend({
6046											fields: 		'creator,keywords,folder:title,webLocation:title,regions',
6047											types:			'all',	
6048											depth: 			'current', 			// 'tree' | 'current' | 'subfolders'
6049											minLength:		1,
6050											divisible:		'comment,title'
6051										}, opt),
6052					tags 			= [], 
6053					fields 			= Array.isArray(options.fields)? options.fields : options.fields.split(/,\s?/), 
6054					fieldslength 	= fields.length,
6055					divisible		= new Array(fieldslength),
6056					allTypes 		= options.types === 'all',
6057					types			= {},
6058					//exact			= {},
6059					
6060					// Add tags collected from an item
6061					// tags = [ 'tag', paths[], 'TAG' ]
6062					
6063					_addTags = function(o, newTags) {
6064							let nt = newTags.split('^').filter(t => t.length >= options.minLength)
6065							
6066							for (let i = 0, idx, tag, p; i < nt.length; i++) {
6067								
6068								tag = nt[i].toUpperCase();
6069								p = getObjectPath(o);
6070								
6071								if (p !== null) {
6072									if (!tags || !tags.length) {
6073										// Empty
6074										tags = [[ nt[i], [ p ], tag ]];
6075									} else { 
6076										// Does this tag exist?
6077										if ((idx = tags.findIndex(t => t[2] === tag)) >= 0) {
6078											// Yes: add path
6079											if (tags[idx][1].indexOf(p) === -1) {
6080												// This items has been added already?
6081												tags[idx][1].push(p);
6082											}
6083										} else {
6084											// No: add new
6085											tags.push([ nt[i], [ p ], tag ]);
6086										}
6087									}
6088								}
6089							}
6090						},
6091					
6092					// Collects tags from an item
6093					
6094					_collectTags = function(o, cat) {
6095							let ctags = '^',			// Collected tags = 'tag1^tag2^tag3'
6096								ctagsuc = '^',			// Same in uppercase for comparison
6097							
6098								add = function(tag, field, divisible) {
6099										
6100										if (!tag) {
6101											return;
6102										}
6103										
6104										let t, 
6105											ta;
6106											
6107										if (divisible) {
6108											if (field === 'comment' || field.endsWith('Caption')) {
6109												tag = tag.stripHTML();
6110											}
6111											//ta = tag.split(/\W+/);
6112											ta = tag.split(/[\s,_\.\?\!\-\(\)\[\]]/);
6113											ta = removeEmpty(ta);
6114										} else {
6115											ta = tag.toString().split(';');
6116										}
6117										
6118										for (let i = 0, l = ta.length, fnd = false; i < l; i++) {
6119										
6120											t = ta[i].trim();
6121											
6122											if (t.length < options.minLength ||
6123												t.length === 1 && '!@#$%^&*()-_=+[{]};:\'",<.>/?\\|'.indexOf(t.charCodeAt(0)) !== -1) {
6124												// Empty, too short, or special char
6125												continue;
6126											}
6127											
6128											if (ctagsuc.indexOf('^' + t.toUpperCase() + '^') === -1) {
6129												ctags += t + '^';
6130												ctagsuc += t.toUpperCase() + '^';
6131											}
6132										}
6133									};
6134							
6135							for (let i = 0, f, p; i < fieldslength; i++) {
6136								
6137								if (fields[i].length > 1) {
6138									if (fields[i][0] !== cat) {
6139										continue;
6140									}
6141									f = fields[i][1];
6142								} else {
6143									f = fields[i][0];
6144								}
6145								
6146								
6147								if (JCAMERAFIELDS.indexOf(f) >= 0 && o.hasOwnProperty(J.CAMERA)) {
6148									// camera data
6149									p = o[J.CAMERA][f];
6150									if (typeof p === UNDEF) {
6151										p = o[f];
6152									}
6153								} else if (f === J.REGIONS) {
6154									// Regions
6155									p = o.hasOwnProperty(J.REGIONS)? JSON.parse(o[J.REGIONS]) : null;
6156									
6157								} else {
6158									p = o[f];
6159								}
6160									
6161								if (typeof p !== UNDEF && p != null) {
6162									//log(o['name'] + '[' + f + '] = ' + o[f] + ' (' + (Array.isArray(o[f])? 'array':(typeof o[f])) + ')');
6163									if (f === J.REGIONS) {
6164										// Checking for the names only
6165										for (let j = 0; j < p.length; j++) {
6166											add(p[j].split(';')[0], f, divisible[i]);
6167										}
6168									} else if (Array.isArray(p)) {
6169										// Array type field
6170										for (let j = 0; j < p.length; j++) {
6171											add(p[j], f, divisible[i]);
6172										}
6173									} else {
6174										add(p, f, divisible[i]);
6175									}
6176								}
6177							}
6178							
6179							//log(ctags);
6180							if (ctags.length > 1) {
6181								_addTags(o, ctags);
6182							}
6183						},
6184					
6185					// Collect tags from all objects in a folder
6186					
6187					_addItems = function(folder) {
6188					
6189							// Adds fields from objects array
6190							if (!folder) {
6191								return;
6192							}
6193							
6194							if (folder !== tree && (allTypes || types['folder'])) {
6195								// Current folder
6196								_collectTags(folder, 'folder');
6197							}
6198							
6199							if (folder.hasOwnProperty(J.OBJECTS)) {
6200								// Ordinary objects
6201								for (let i = 0, o = folder[J.OBJECTS], cat; i < o.length; i++) {
6202									if (o[i].hasOwnProperty(J.CATEGORY)) {
6203										cat = o[i][J.CATEGORY];
6204										if (allTypes || types[cat]) {
6205											_collectTags(o[i], cat);
6206										}
6207									}
6208								}
6209							}
6210						},
6211				
6212					// Queues one folder to collect tags  
6213					
6214					_addFolder = function(folder) {
6215							// Adding one folder
6216							
6217							if (!folder || (folder.hasOwnProperty(J.HIDDEN) && folder.hidden)) {
6218								return;
6219							}
6220							
6221							onFolderReady(folder, _addItems);
6222							
6223							if (isDeep()) {
6224								if (folder.hasOwnProperty(J.OBJECTS)) {
6225									folder[J.OBJECTS].forEach(o => {
6226											if (o[J.CATEGORY] === 'folder' && !(o.hasOwnProperty(J.HIDDEN) && o[J.HIDDEN]) && !o.hasOwnProperty(J.VIEW)) {
6227												_addFolder(o);
6228											}
6229										});
6230								}
6231							} else if (options.depth !== 'current' && folder.hasOwnProperty(J.FOLDERS)) {
6232								folder[J.FOLDERS].forEach(o => {
6233										if (!(o.hasOwnProperty(J.HIDDEN) && o[J.HIDDEN]) && !o.hasOwnProperty(J.VIEW)) {
6234											_addFolder(o);
6235										}
6236									});
6237							}
6238						},
6239						
6240					// Arrange the tags when ready
6241					
6242					_finally = function() {
6243							// Sort
6244							if (options.sort === 'name') {
6245								tags.sort((a, b) => ('' + a[2]).localeCompare('' + b[2]));	
6246							} else if (options.sort === 'frequency') {
6247								tags.sort((a, b) => (b[1].length - a[1].length));
6248							}
6249							// Max.
6250							if (options.max && options.max < tags.length) {
6251								tags = tags.slice(0, options.max);
6252							}
6253							// Done functions
6254							options.ready.call(tags, options);
6255						};
6256				
6257				// Starting a new promise collect
6258				
6259				defer = [];
6260				
6261				// Determining exact/divisible fields
6262				
6263				for (let i = 0, f, d = options.divisible.split(','); i < fieldslength; i++) {
6264					fields[i] = fields[i].split(':');
6265					divisible[i] = d.indexOf(fields[i][1] || fields[i][0]) !== -1;
6266				}
6267				
6268				// Creating object types array too look for
6269				
6270				if (!allTypes) {
6271					if (options.types.charAt(0) === '-') {
6272						// Negative
6273						settings.possibleTypes.forEach(t => {
6274								if (options.types.indexOf(t) === -1) {
6275									types[t] = true;
6276								}
6277							});
6278					} else {
6279						// Positive
6280						options.types.split(/,\s?/).forEach(t => { types[t] = true; });
6281					}
6282				}
6283				
6284				// Adding folder(s)
6285				
6286				_addFolder((options.depth === 'tree')? tree : currentFolder);
6287				
6288				if (defer.length) {
6289					$.when.apply($, defer).done(_finally);
6290				} else {
6291					_finally();
6292				}
6293						
6294			},
6295		
6296		/*
6297		 * Getting one key from an object in a processed form, e.g. fileLabel variables are handled
6298		 */
6299		
6300		getKey = function(o, k) {
6301				
6302				if (typeof o !== UNDEF && typeof k === STRING) {
6303					if (k === 'label') {
6304						return getLabel(o);
6305					} else if (k === 'fileLabel') {
6306						return o.hasOwnProperty(J.TITLE)? o[J.TITLE] : getLabel(o);
6307					} else if (k === 'originalTime' &&
6308							o.hasOwnProperty(J.DATES) && 
6309							o[J.DATES].hasOwnProperty(J.DATETAKEN)
6310						) {
6311						return new Date(o[J.DATES][J.DATETAKEN] * 1000).toLocaleString(settings.locale, {month: '2-digit', day: '2-digit', hour12: false, hour: '2-digit', minute:'2-digit'});
6312					} else if (o.hasOwnProperty(k)) {
6313						return o[k];
6314					}
6315				}
6316				
6317				return ''; 
6318			},
6319			
6320		/***************************************************************
6321		 *
6322		 *	processTemplate:	Processing template for an object
6323		 *		template:		string with variables in format ${name} or in ${var1|var2} fallback format
6324		 *		co:				current object
6325		 *		removeEmpty:	removing empty tags around missing/empty variables
6326		 */
6327		
6328		processTemplate = function(template, co, removeEmpty) {
6329			
6330				let remove = (typeof removeEmpty !== UNDEF)? removeEmpty : false,
6331					o = co || currentFolder,
6332					i0,
6333					i1,
6334					m,
6335					v;
6336				
6337				if (template && template.indexOf('${') >= 0) {
6338				
6339					while (m = template.match(/\$\{([\w\.|]+)\}/)) {
6340						if (m[1].indexOf('|') > 0) {
6341							// ${var1|var2} fallback format
6342							for (let i = 0, k = m[1].split('|'); i < k.length; i++) {
6343								if (v = getKey(o, k[i])) {
6344									// Found
6345									break; 
6346								}
6347							}
6348						} else {
6349							// Single variable
6350							v = getKey(o, m[1]);
6351						}
6352						
6353						if (v === null && remove) {
6354							// Remove empty HTML tags
6355							i0 = m.index - 1;
6356							i1 = i0 + m[0].length;
6357							
6358							if (i0 > 0 && template[i0] === '>' && i1 < (template.length - 1) && template[i1] === '<') {
6359								
6360								i0 = template.lastIndexOf('<', i0);
6361								i1 = template.indexOf('>', i1);
6362								
6363								if (i0 >= 0 && i1 >= 0) {
6364									template = template.slice(0, i0) + template.slice(i1);
6365									continue;
6366								}
6367							}
6368						}
6369						// Replacing or removing variable
6370						template = template.slice(0, m.index) + (v || '') + template.slice(m.index + m[0].length);
6371					}
6372				}
6373		
6374				return template;
6375			},
6376									
6377		
6378		/***************************************************************
6379		 *
6380		 * 				Initial processing of the album
6381		 *
6382		 */
6383		
6384		_up = '../../../../../../../../../../../../../../../../../../../../',
6385		
6386		relativePath = function(from, to) {
6387				if (typeof from === UNDEF || !from.length || from === '/') {
6388					// From root
6389					return to || '';
6390				} else if (typeof to === UNDEF || !to.length || to === '/') {
6391					// To root
6392					return _up.slice(0, from.split('/').length * 3);
6393				} else if (from === to) {
6394					// Same
6395					return '';
6396				}
6397				
6398				from = from.split('/');
6399				to = to.split('/');
6400				
6401				while (from.length && to.length && from[0] === to[0]) {
6402					from.shift();
6403					to.shift();
6404				}
6405				
6406				return _up.slice(0, from.length * 3) + to.join('/');
6407			},
6408			
6409		/*
6410		 *	Adding level, parent pointers and relative paths for the whole tree
6411		 */
6412		 
6413		addExtras = function() {
6414				let addFolder = function(f, level, path, parentRef) {
6415						let fp = level? makePath(path, f[J.PATH]) : '',		// folder path: subfolder/subfolder/
6416							pr = level? getPathRef(fp) : 0,					// path reference number
6417							rp = (albumPath === null)? getRelPathRef(relativePath(settings.relPath, fp)) : -1;	// Relative path
6418						
6419						// Enhancing folders with extra variables
6420						
6421						// Level
6422						f[J.LEVEL] = level;
6423						
6424						// Folder path reference from root
6425						f[J.PATHREF] = pr;
6426						
6427						// Relative path from currentFolder to this folder
6428						if (albumPath === null) {
6429							// if we're inside the album (providing simpler paths)
6430							f[J.RELPATH] = rp;
6431						}
6432						
6433						// Check for missing Category
6434						if (!f.hasOwnProperty(J.CATEGORY)) {
6435							f[J.CATEGORY] = 'folder';
6436						}
6437						
6438						// Parent reference
6439						if (level) {
6440							f[J.PARENTREF] = parentRef;
6441						}
6442						
6443						// Fixing tree.json anomaly:
6444						// thumb path is different in deep-data.json and data1.json
6445						if (f[J.THUMB][J.PATH].startsWith(f[J.PATH] + '/' + settings.thumbsDir)) {
6446							f[J.THUMB][J.PATH] = f[J.THUMB][J.PATH].slice(f[J.PATH].length + 1);
6447						}
6448						
6449						// Recursive to subfolders
6450						if (settings.loadDeep && f.hasOwnProperty(J.OBJECTS)) {
6451							// Adding path references in case of deep-data.json
6452							f[J.OBJECTS].filter(o => o[J.CATEGORY] !== 'folder').forEach((o) => {
6453									o[J.PATHREF] = pr;
6454									if (albumPath === null) {
6455										o[J.RELPATH] = rp;
6456									}
6457								});
6458							
6459							// Collecting folders' indexes into FOLDERS array for easier access later 
6460							let idx = 0;
6461							f[J.OBJECTS].filter(o => o[J.CATEGORY] === 'folder').forEach(o => {
6462									if (!idx) {
6463										f[J.FOLDERS] = [];
6464									}	
6465									f[J.FOLDERS].push(idx++);
6466									// Recursive call for subfolders
6467									addFolder(o, level + 1, fp, pr);
6468								});
6469						} else {
6470							// Iterating over folders
6471							if (f.hasOwnProperty(J.FOLDERS)) {
6472								f[J.FOLDERS].forEach(o => addFolder(o, level + 1, fp, pr));
6473							}
6474						}
6475					};
6476				
6477				addFolder(tree, 0, '', 0);
6478			},
6479			
6480		/*
6481		 *	callOnReady: Calling cached onready functions and removing them
6482		 */
6483		 
6484		callOnReady = function(folder) {
6485				// firing all cached onFolderReady functions
6486				//log('Firing ' + folder[J.ONREADY].length + ' cached onReady function(s) in folder "' + folder[J.NAME] + '"');
6487				if (folder.hasOwnProperty(J.ONREADY) && Array.isArray(folder[J.ONREADY])) {
6488					folder[J.ONREADY].forEach(rfn => {
6489							if (rfn.length > 1) {
6490								// With arguments
6491								rfn[0](folder, rfn[1]);
6492							} else {
6493								// No arguments
6494								rfn[0](folder);
6495							}
6496						});
6497					folder[J.ONREADY] = [];
6498				}
6499			},
6500			
6501		/*
6502		 *	addObjects: Adding Objects array to tree when using data1.json
6503		 *		executed after the folder's data file has loaded
6504		 *		d = JSON data loaded
6505		 *		folder = target folder
6506		 */				
6507		
6508		addObjects = function(folder, d) {
6509				let j = 0,			// Counting folders
6510					f;
6511					
6512				// Ensure it exists
6513				folder[J.OBJECTS] = [];
6514				
6515				//log('Adding objects in folder "' + (folder[J.NAME] || folder[J.PATH]) + '"');
6516					
6517				d[J.OBJECTS].forEach(o => {
6518						// Counting loaded object categories
6519						tree[J.LOADCOUNTER][o[J.CATEGORY]]++;
6520						tree[J.LOADCOUNTER][J.TOTAL]++;
6521						
6522						if (o[J.CATEGORY] === 'folder') {
6523							// Folder
6524							// Proper way:
6525							//o = { folderIndex: folder[J.FOLDERS].findIndex(f => o[J.PATH] === f[J.PATH]) };
6526							// Heuristic: (provided tree.json has the same folder ordering as data1.json)
6527							if (j < folder[J.FOLDERS].length) {
6528								
6529								f = folder[J.FOLDERS][j];
6530								
6531								// Copying missing folder variables but OBJECTS and ALBUM
6532								for (let prop in o) {
6533									if (prop !== J.OBJECTS && prop !== J.ALBUM && !f.hasOwnProperty(prop)) {
6534										f[prop] = o[prop];
6535									}
6536								}
6537								
6538								// J.FOLDERS array already holds the full info, storing only a pointer
6539								o = {};
6540								o[J.FOLDERINDEX] = j++;
6541								
6542							} else {
6543							
6544								log('Database error: Folder count inconsistency in folder "' + folder[J.NAME] + '". tree.json <> data1.json');
6545							}
6546							
6547						} else {
6548							// Not a folder
6549							// Adding absolute and relative paths
6550							o[J.PATHREF] = folder[J.PATHREF];
6551							o[J.RELPATH] = folder[J.RELPATH];
6552						}
6553						// Storing object in the tree
6554						folder[J.OBJECTS].push(o);
6555					});
6556				
6557				folder[J.READY] = true;
6558				
6559				callOnReady(folder);
6560			},
6561			
6562		/*
6563		 *	onFolderReady: Checks if folder data has been loaded 
6564		 *		if loaded executes the function immediately 
6565		 *		if not then saves the function for later execution 
6566		 *		fn - is the folder to be called on ready
6567		 *		params - the parameters array (optional)
6568		 */
6569		
6570		onFolderReady = function(folder, fn, params) {
6571			
6572				if (folder && typeof fn === FUNCTION && !folder.hasOwnProperty(J.INTERNALLINK)) {
6573					
6574					if (deepReady || (folder.hasOwnProperty(J.READY) && folder[J.READY])) {
6575						// Already loaded: fire immediately
6576						if (typeof params !== UNDEF) {
6577							fn(folder, params);
6578						} else {
6579							fn(folder);
6580						}
6581					} else {
6582						// Need to cache
6583						//log('Caching onReady function for folder "' + folder[J.NAME] + '" ' + (folder.hasOwnProperty(J.ONREADY)? ('[' + (folder[J.ONREADY].length + 1) + ']') : '(onReady array is missing)'));
6584						if (!folder.hasOwnProperty(J.ONREADY)) {
6585							// Loading hasn't been initiated, let's load it!
6586							loadFolder(folder);
6587						}
6588						
6589						if (typeof params !== UNDEF) {
6590							folder[J.ONREADY].push([ fn, Array.isArray(params)? params : [ params ] ]);
6591						} else {
6592							folder[J.ONREADY].push([ fn ]);
6593						}
6594					}
6595				}
6596			},
6597			
6598		/*
6599		 *	loadFolder: Loading data for a single folder, optionally recursively
6600		 */
6601		
6602		loadFolder = function(folder, deep) {
6603			
6604				//log('LoadFolder("' + folder[J.NAME] + '")');
6605				
6606				// Don't bother with lkinked folders
6607				if (folder.hasOwnProperty(J.INTERNALLINK)) {
6608					return;
6609				}
6610				
6611				if (!folder.hasOwnProperty(J.ONREADY)) {
6612					// Creating an empty array for onready functions, also signaling that the load has been initiated
6613					folder[J.ONREADY] = [];
6614				}
6615				
6616				if (!folder.hasOwnProperty(J.OBJECTS) || !folder[J.OBJECTS].length) {
6617					// we need to load data1.json
6618					let src = makePath(getPath(folder), settings.dataFile) + cacheBuster;
6619					
6620					// building defer array to be able to check the full load
6621					if (!defer) {
6622						defer = [];
6623					}
6624					//log('Loading objects for folder "' + folder[J.NAME] + '"');
6625					// Cache buster with ?makeDate
6626					defer.push($.getJSON(src)
6627							.done(function(d) {
6628								//log('Objects loaded for folder "' + folder[J.NAME] + '"');
6629								// Copying objects
6630								addObjects(folder, d);
6631								
6632							}).fail(function(jqxhr, status, error) {
6633								log('Error: Objects could not be loaded for "' + src + '": ' + status + ', ' + error);
6634							})
6635						);
6636					
6637					if (deep && folder.hasOwnProperty(J.FOLDERS)) {
6638						folder[J.FOLDERS].forEach(f => loadFolder(f, true));
6639					}
6640					
6641				} else {
6642					callOnReady(folder);
6643				}
6644			},
6645			
6646		/*
6647		 *	initCounters: Initializing tree-level counters
6648		 */
6649		
6650		initCounters = function() {
6651			
6652				if (tree) {
6653					// Initializing the load counters
6654					tree[J.LOADCOUNTER] = {};
6655					tree[J.LOADCOUNTER][J.TOTAL] = 0;
6656					
6657					settings.possibleTypes.forEach(t => {
6658							tree[J.LOADCOUNTER][t] = 0;
6659						});
6660				}
6661			},
6662			
6663		/*
6664		 *	setCurrentFolder: Sets currentFolder variable as a shortcut
6665		 */
6666		
6667		setCurrentFolder = function() {
6668				// Getting the pointer to the current folder
6669				currentFolder = getFolder(settings.relPath);
6670				
6671				if (currentFolder === null) {
6672					if (typeof settings.fatalError === FUNCTION) {
6673						settings.fatalError.call(this, 'noSuchFolder', settings.relPath);
6674					}
6675					
6676					return false;
6677				}
6678				
6679				return true;
6680			},
6681							
6682		/*
6683		 *	loadNeededFolders: executed when tree.json has loaded
6684		 */
6685		
6686		loadNeededFolders = function() {
6687					
6688				// Collecting promises for data1.json loads
6689				defer = [];
6690				
6691				// Loading current folder (+ deep folders?)
6692				loadFolder(settings.lazy? currentFolder : tree, !settings.lazy);
6693				
6694				// Waiting for all requested folders be loaded
6695				$.when.apply($, defer)
6696					.done(function() {
6697							let d = new Date();
6698							
6699							if (typeof DEBUG !== UNDEF && DEBUG) {
6700								log(defer.length + ' folder(s) loaded: ' + (d - instance) + 'ms');
6701							}
6702							
6703							ready = true;
6704							defer = null;
6705							
6706							if (typeof settings['ready'] === FUNCTION) {
6707								settings.ready.call(this);
6708								settings.ready = null;
6709							} else if (!tree.hasOwnProperty(J.FOLDERS)) {
6710								// Flat album: calling deep ready immediately
6711								if (typeof settings['deepReady'] === FUNCTION) {
6712									settings.deepReady.call(this);
6713									settings.deepReady = null;
6714								}
6715							}
6716						})
6717					
6718					.fail(function(jqxhr, status, error) {
6719							
6720							if (typeof settings['fatalError'] === FUNCTION) {
6721								settings.fatalError.call(this, 'cantLoadDataForFolder', currentFolder[J.PATH]);
6722							}
6723							
6724						});
6725			},
6726			
6727		/*
6728		 *	initFolderReady: initializing ready state and ready events in a recursive manner (in case tree.json was loaded)
6729		 */
6730		
6731		initFolderReady = function(folder) {
6732			
6733				// Sets READY property
6734				if (!folder.hasOwnProperty(J.READY)) {
6735					folder[J.READY] = false;
6736				}
6737				
6738				// Go deep recursively into subfolders
6739				if (settings.loadDeep) {
6740					if (folder.hasOwnProperty(J.OBJECTS)) {
6741						folder[J.OBJECTS].forEach(o => { if (o[J.CATEGORY] === 'folder') initFolderReady(o); });
6742					}
6743				} else if (folder.hasOwnProperty(J.FOLDERS)) {
6744					folder[J.FOLDERS].forEach(f => initFolderReady(f));
6745				}
6746			},
6747						
6748		/*
6749		 *	loadTree: Loading tree.json from the top level folder
6750		 */
6751		
6752		loadTree = function(doneFn) {
6753			
6754				let ins = new Date(),
6755					src = makePath(albumPath || settings.rootPath, settings.treeFile) + cacheBuster;
6756				
6757				// Set loadDeep to false whatever it was. Means tree.json is used, not deep-data.json
6758				settings.loadDeep = false;
6759				//log('loadTree("' + src + '")');
6760				
6761				// Loading tree.json
6762				return $.getJSON(src)
6763					.done(function(d) {
6764							// tree.json loaded
6765							
6766							tree = d;
6767							
6768							// Adding counters
6769							initCounters();
6770							
6771							// Initializing ready state and onready events in every folder
6772							initFolderReady(tree);
6773							
6774							if (typeof DEBUG !== UNDEF && DEBUG) {
6775								log('Tree loaded: ' + ((new Date()) - ins) + 'ms');
6776								ins = new Date();
6777							}
6778							
6779							if (setCurrentFolder()) {
6780							
6781								// Adding extra variables
6782								addExtras();
6783								
6784								// Calling "done" function
6785								if (typeof doneFn === FUNCTION) {
6786									doneFn.call(this);
6787								}
6788							}
6789							
6790						})
6791					
6792					.fail(function(jqxhr, status, error) {
6793								
6794							if (typeof settings['fatalError'] === FUNCTION) {
6795								settings.fatalError.call(this, 'localAccessBlocked', src);
6796							}
6797							
6798						});
6799			},
6800			
6801		/*
6802		 *	migrateTree: moves data from tree to the new deep-data structure
6803		 *		used when deep data is loaded after tree used initially 
6804		 */
6805		 
6806		initDeep = function(d) {
6807				
6808				// Set loadDeep to true whatever it was. Means deep-data.json is used, not tree.json
6809				settings.loadDeep = true;
6810				
6811				if (typeof tree === UNDEF) {
6812					
6813					// First load
6814					tree = d;
6815					
6816					tree[J.DEEP] = true;
6817					
6818					// Adding counters
6819					initCounters();
6820					
6821					// Enhancing all the folders with PATH and RELPATH variables, counting object types
6822					addExtras();
6823					
6824				} else {
6825					
6826					// Migrating from tree.json
6827					let	oldTree = tree,
6828						migrateFolder = function(oldFolder, newFolder) {
6829								//log('Migrating folder "' + oldFolder[J.NAME] + '"');
6830								// Copying missing properties (e.g. addExtras)
6831								for (let prop in oldFolder) {
6832									if (prop !== J.FOLDERS && prop !== J.ALBUM && 
6833										prop !== J.OBJECTS && prop !== J.READY && 
6834										prop !== J.ONREADY && !newFolder.hasOwnProperty(prop)) {
6835										//log('Copying property "' + prop + '".');
6836										newFolder[prop] = oldFolder[prop];
6837									}
6838								}
6839								
6840								// Firing ready functions attached to old folder
6841								if (oldFolder.hasOwnProperty(J.ONREADY)) {
6842									//log('Firing ' + oldFolder[J.ONREADY].length + ' ready events.');
6843									oldFolder[J.ONREADY].forEach(rfn => (rfn.length > 1)? rfn[0](newFolder, rfn[1]) : rfn[0](newFolder));
6844								}
6845								
6846								if (newFolder.hasOwnProperty(J.OBJECTS)) {
6847									let pr = oldFolder[J.PATHREF],
6848										rp = oldFolder[J.RELPATH] || null,
6849										idx = 0;
6850										
6851									//log('Adding extras to "' + oldFolder[J.NAME] + '".');	
6852									// Adding path references in case of deep-data.json
6853									// Collecting folders' indexes into FOLDERS array for easier access later 
6854									newFolder[J.OBJECTS].forEach(o => {
6855											if (o[J.CATEGORY] !== 'folder') {
6856												o[J.PATHREF] = pr;
6857												if (rp) {
6858													o[J.RELPATH] = rp;
6859												}
6860											} else {
6861												if (!idx) {
6862													newFolder[J.FOLDERS] = [];
6863												}	
6864												newFolder[J.FOLDERS].push(idx++);
6865											}
6866										});
6867								
6868									if (oldFolder.hasOwnProperty(J.FOLDERS)) {
6869										oldFolder[J.FOLDERS].forEach(f => {
6870												// Recursive call for subfolders
6871												const nf = newFolder[J.OBJECTS].find(o => (o[J.CATEGORY] === 'folder' && o[J.PATH] === f[J.PATH]));
6872												if (nf) {
6873													migrateFolder(f, nf);
6874												} else {
6875													log('Database inconsistency when accessing folder "' + oldFolder[J.NAME] + '"!');
6876												}
6877											});
6878									}
6879								}
6880								
6881							};
6882							
6883					// Replacing tree with deep data 
6884					tree = d;
6885					
6886					// Deep structure flag
6887					tree[J.DEEP] = true;
6888					
6889					// Migrating the missing properties
6890					migrateFolder(oldTree, tree);
6891				}					
6892			},
6893			
6894		/*
6895		 *	onDeepReady: Checks if all data has been loaded 
6896		 *		if loaded, executes the function immediately 
6897		 *		if not, then saves the function for later execution 
6898		 *		fn - is the folder to be called on ready
6899		 *		params - the parameters array (optional)
6900		 */
6901		
6902		onDeepReady = function(folder, fn, params) {
6903			
6904				if (folder && typeof fn === FUNCTION) {
6905					
6906					if (deepReady) {
6907						// Already loaded: fire immediately
6908						if (typeof params !== UNDEF) {
6909							fn(folder, params);
6910						} else {
6911							fn(folder);
6912						}
6913					} else {
6914						// Need to cache
6915						//log('Caching onReady function for folder "' + folder[J.NAME] + '" ' + (folder.hasOwnProperty(J.ONREADY)? ('[' + (folder[J.ONREADY].length + 1) + ']') : '(onReady array is missing)'));
6916						
6917						if (typeof params !== UNDEF) {
6918							deepReadyFunctions.push([ folder, fn, Array.isArray(params)? params : [ params ] ]);
6919						} else {
6920							deepReadyFunctions.push([ folder, fn ]);
6921						}
6922					}
6923				}
6924			},
6925			
6926		/*
6927		 *	loadDeep: Loads deep data structure
6928		 */
6929		
6930		loadDeep = function(doneFn, failFn) {
6931				
6932				if (deepReady) {
6933					// Already loaded 
6934					//log('loadDeep(): already in "deepReady" state');
6935					doneFn.call(tree);
6936					return;
6937				}
6938				
6939				let ins = new Date(),
6940					src = makePath(albumPath || settings.rootPath, settings.deepDataFile) + cacheBuster;
6941				
6942				//log('loadDeep("' + src + '")');
6943				
6944				// Loading deep-data.json
6945				return $.getJSON(src)
6946					.done(function(d) {
6947							// deep-data.json loaded
6948							
6949							if (typeof DEBUG !== UNDEF && DEBUG) {
6950								log('Deep data loaded: ' + ((new Date()) - ins) + 'ms');
6951								ins = new Date();
6952							}
6953							
6954							initDeep(d);
6955							
6956							// Setting current folder variable
6957							if (setCurrentFolder()) {
6958							
6959								deepReady = true;
6960								/*
6961								if (typeof DEBUG !== UNDEF && DEBUG) {
6962									log('Deep data objects are ready: ' + ((new Date()) - ins) + 'ms' + ' total: ' + tree[J.LOADCOUNTER][J.TOTAL] + ' objects');
6963								}
6964								*/
6965								if (typeof doneFn === FUNCTION) {
6966									doneFn.call(this);
6967								} else {
6968									// Calling folder ready function
6969									if (typeof settings['ready'] === FUNCTION) {
6970										settings.ready.call(this);
6971										settings.ready = null;
6972									}
6973									
6974									// Calling deep ready function 
6975									if (typeof settings['deepReady'] === FUNCTION) {
6976										settings.deepReady.call(this);
6977										settings.deepReady = null;
6978									}
6979								}
6980								
6981								if (deepReadyFunctions.length) {
6982									// [ folder, readyFn, arguments ]
6983									deepReadyFunctions.forEach(rfn => (rfn.length > 2)? rfn[1](rfn[0], rfn[2]) : rfn[1](rfn[0]));
6984									deepReadyFunctions = [];
6985								}
6986							}
6987							
6988						})
6989					.fail(function(jqxhr, status, error) {
6990						
6991							if (typeof settings['fatalError'] === FUNCTION) {
6992								settings.fatalError.call(this, 'databaseAccessDenied', src);
6993							}
6994							
6995							if (typeof failFn === FUNCTION) {
6996								failFn.call(this);
6997							}
6998							
6999						});
7000			},
7001			
7002		/***************************************************************
7003		 *
7004		 *						Initializing
7005		 *
7006		 */
7007		
7008		init = opt => {
7009			
7010				if (instance) {
7011					// Already initialized
7012					return instance;
7013				}
7014				
7015				// Unique ID
7016				instance = new Date();
7017				
7018				if (typeof opt !== UNDEF) {
7019					// Passed options
7020					$.extend(settings, opt);
7021				}
7022				
7023				// Resetting ready state
7024				ready = deepReady = false;
7025				
7026				// using albumPath: calling Album from outside
7027				if (settings.hasOwnProperty('albumPath')) {
7028					
7029					// Initializing by absolute URL
7030					albumPath = settings.albumPath;
7031					
7032					// Sanitizing URL
7033					if (albumPath.slice(-1) !== '/') {
7034						albumPath += '/';
7035					}
7036				}
7037				
7038				// Assigning absolute path
7039				absolutePath = getAbsoluteFolderPath(albumPath || settings.rootPath);
7040				
7041				// Using makeDate to get rid of the old version of JSON files
7042				if (settings.hasOwnProperty('makeDate')) {
7043					cacheBuster = '?' + settings.makeDate;
7044				}
7045				
7046				// Loading album's JSON file
7047				if (settings.loadDeep || settings.hasOwnProperty('deepReady')) {
7048					// Loading deep-data.json 
7049					loadDeep();
7050				} else {
7051					// Loading tree.jsonm when ready loading the current folder
7052					loadTree(loadNeededFolders);
7053				}
7054						
7055			};
7056		
7057	
7058	if (options) {
7059		
7060		if (typeof DEBUG !== UNDEF && DEBUG) {
7061			log('new Album(' + JSON.stringify(options) + ');');
7062		}
7063		
7064		init(options);
7065	}
7066	
7067	// Exposing internal functions
7068	
7069	return {
7070			//init: 							init,
7071			//isReady:						isReady,
7072			//isDeepReady:					isDeepReady,
7073			
7074			// Debug
7075			//getTree: 						getTree,
7076			//getPaths: 						getPaths,
7077			
7078			// Type checking
7079			isImage: 						isImage,
7080			isAudio: 						isAudio,
7081			isVideo: 						isVideo,
7082			isPage: 						isPage,
7083			isFolder:						isFolder,
7084			isView:							isView,
7085			isLightboxable: 				isLightboxable,
7086			isCurrentFolder: 				isCurrentFolder,
7087			
7088			// Access
7089			getAlbumPath:					getAlbumPath,
7090			getAlbumRootPath:				getAlbumRootPath,
7091			getPath:						getPath,
7092			getAbsolutePath:				getAbsolutePath,
7093			getItemPath:					getItemPath,
7094			getAudioClipPath:				getAudioClipPath,
7095			getDimensions:					getDimensions,
7096			getOriginalDimensions:			getOriginalDimensions,
7097			getMaxDimensions:				getMaxDimensions,
7098			getAspectRatio:					getAspectRatio,
7099			getRegions:						getRegions,
7100			getLink:						getLink,
7101			getRootPath:					getRootPath,
7102			getFolderPath:					getFolderPath,
7103			getRelativeFolderPath:			getRelativeFolderPath,
7104			getFolder: 						getFolder,
7105			getCurrentFolder:				getCurrentFolder,
7106			getParent: 						getParent,
7107			getItem:						getItem,
7108			getObjects: 					getObjects,
7109			getImages: 						getImages,
7110			getFolders:						getFolders,
7111			getFoldersAndWeblocs:			getFoldersAndWeblocs,
7112			getAllObjects:					getAllObjects,
7113			
7114			// Properties
7115			getMakeDate: 					getMakeDate,
7116			getAlbumTitle: 					getAlbumTitle,
7117			getItemName:					getItemName,
7118			getExtension:					getExtension,
7119			getLevel: 						getLevel,
7120			getTitle: 						getTitle,
7121			getName: 						getName,
7122			getLabel: 						getLabel,
7123			getAlt:							getAlt,
7124			getComment: 					getComment,
7125			getThumbPath: 					getThumbPath,
7126			getIconPath: 					getIconPath,
7127			getImagePath: 					getImagePath,
7128			getAbsoluteImagePath:			getAbsoluteImagePath,
7129			getThemeImagePath:				getThemeImagePath,
7130			getOriginalPath:				getOriginalPath,
7131			getOriginalPageName:			getOriginalPageName,
7132			getVideoPath:					getVideoPath,
7133			getPosterPath: 					getPosterPath,
7134			getOptimalImagePath:			getOptimalImagePath,
7135			getOptimalThumbPath:			getOptimalThumbPath,
7136			getSourcePath: 					getSourcePath,
7137			getAbsoluteItemPath: 			getAbsoluteItemPath,
7138			getVideoDuration:				getVideoDuration,
7139			hasShop: 						hasShop,
7140			hasLocation: 					hasLocation,
7141			getLocation: 					getLocation,
7142			disableFeedback:				disableFeedback,
7143			hideFotomoto:					hideFotomoto,
7144			getPriceRange:					getPriceRange,
7145			getCurrency:					getCurrency,
7146			getDeepFolderCount:				getDeepFolderCount,
7147			getCustomPageContent:			getCustomPageContent,
7148			
7149			// Generic property
7150			getRootProperty: 				getRootProperty,
7151			getInheritedPropertyObject:		getInheritedPropertyObject,
7152			getInheritedProperty:			getInheritedProperty,
7153			getProperty: 					getProperty,
7154			getPropertyObject:				getPropertyObject,
7155			
7156			getNextFoldersFirstImage:		getNextFoldersFirstImage,
7157			getPreviousFoldersLastImage:	getPreviousFoldersLastImage,
7158			
7159			sortItems:						sortItems,
7160			
7161			// Search
7162			collectByPath:					collectByPath,
7163			collectNItem:					collectNItem,
7164			collectByDate: 					collectByDate,
7165			collectItems: 					collectItems,
7166			collectTags: 					collectTags,
7167			
7168			processTemplate:				processTemplate
7169				
7170		};
7171		
7172};
7173/*
7174 *	cookie() :: Cookie handling - using localStorage if exists
7175 *
7176 *	Copyright by Lazaworx
7177 *	http://www.lazaworx.com
7178 *	Author: Laszlo Molnar
7179 *
7180 *	Dual licensed under the MIT and GPL licenses.
7181 *	- http://www.opensource.org/licenses/mit-license.php
7182 *	- http://www.gnu.org/copyleft/gpl.html
7183 *
7184 *	Usage:
7185 *		$.cookie( key )               :: returns cookie value or null  (scoped to DIR_PATH)
7186 *		$.cookie( key, null )         :: deletes cookie                (scoped to DIR_PATH)
7187 *		$.cookie( key, value [,exp] ) :: saves cookie, exp in seconds, default 1 hour
7188 *
7189 *		$.albumCookie( key )               :: same as above, but always globally scoped (path '/')
7190 *		$.albumCookie( key, null )         ::
7191 *		$.albumCookie( key, value [,exp] ) ::
7192 *
7193 *	If DIR_PATH is defined, $.cookie uses that directory; otherwise falls back to '/'.
7194 *	$.albumCookie always uses '/' regardless of DIR_PATH.
7195 */
7196
7197(function($) {
7198	'use strict';
7199
7200	var LOCALSTORAGE = (function() {
7201			try {
7202				localStorage.setItem('_t', '1');
7203				localStorage.removeItem('_t');
7204				return true;
7205			} catch(e) {
7206				return false;
7207			}
7208		}()),
7209
7210		DIR_SCOPE  = (typeof DIR_PATH !== 'undefined' && DIR_PATH) ? DIR_PATH : '/',
7211		GLOBAL_SCOPE = '/',
7212
7213		COOKIE_SEP = '; @',
7214
7215		// Parse a stored string value into its native type
7216		getVal = function(v) {
7217			return (/^(true|yes)$/).test(v)  ? true  :
7218			       (/^(false|no)$/).test(v)  ? false :
7219			       (/^[\d.]+$/).test(v)      ? parseFloat(v) : v;
7220		},
7221
7222		// Deserialize a raw cookie string (handles JSON objects/arrays)
7223		cookieVal = function(v) {
7224			if (typeof v !== 'string') {
7225				return v;
7226			}
7227			if (v[0] === '[' || v[0] === '{') {
7228				v = JSON.parse(v);
7229				for (var o in v) {
7230					if (Object.prototype.hasOwnProperty.call(v, o) && typeof v[o] === 'string') {
7231						v[o] = getVal(v[o]);
7232					}
7233				}
7234				return v;
7235			}
7236			return getVal(v);
7237		},
7238
7239		// Core read/write/delete — path determines scope
7240		_cookie = function(path, key, value, expire) {
7241			var encodedKey = encodeURIComponent(key),
7242				lsKey      = path + encodedKey + '=',
7243				d          = new Date();
7244
7245			if (arguments.length > 2) {
7246				// --- WRITE / DELETE ---
7247
7248				if (value === null) {
7249					// Delete
7250					if (LOCALSTORAGE) {
7251						localStorage.removeItem(lsKey);
7252					} else {
7253						document.cookie = encodedKey + '=; expires=' + d.toGMTString() +
7254						                  '; path=' + path + '; SameSite=Strict';
7255					}
7256					return null;
7257
7258				} else {
7259					// Serialize value
7260					var stored = (typeof value === 'object') ? JSON.stringify(value) : String(value);
7261
7262					// Compute expiry
7263					var seconds = (typeof expire === 'number') ? expire : 3600;
7264					d.setTime(d.getTime() + seconds * 1000);
7265
7266					if (LOCALSTORAGE) {
7267						localStorage.setItem(lsKey, stored + COOKIE_SEP + String(d.getTime()));
7268					} else {
7269						document.cookie = encodedKey + '=' + encodeURIComponent(stored) +
7270						                  '; expires=' + d.toGMTString() +
7271						                  '; path=' + path + '; SameSite=Strict';
7272					}
7273					return value;
7274				}
7275
7276			} else {
7277				// --- READ ---
7278
7279				if (LOCALSTORAGE) {
7280					var raw = localStorage.getItem(lsKey);
7281					if (raw) {
7282						var parts = raw.split(COOKIE_SEP);
7283
7284						// Legacy entries stored without our separator (plain '; ' split)
7285						if (parts.length === 1) {
7286							parts = raw.split('; ');
7287						}
7288
7289						if (parts.length > 1) {
7290							if (d.getTime() < parseInt(parts[1], 10)) {
7291								return cookieVal(parts[0]);   // not yet expired
7292							} else {
7293								localStorage.removeItem(lsKey); // expired — clean up
7294							}
7295						} else {
7296							return cookieVal(parts[0]);   // no expiry stored
7297						}
7298					}
7299
7300				} else {
7301					var cookies = document.cookie.split(/;\s*/),
7302						prefix  = encodedKey + '=',
7303						i;
7304
7305					for (i = 0; i < cookies.length; i++) {
7306						if (cookies[i].substring(0, prefix.length) === prefix) {
7307							return cookieVal(decodeURIComponent(cookies[i].substring(prefix.length)));
7308						}
7309					}
7310				}
7311			}
7312
7313			return null;
7314		};
7315
7316	// --- Public API ---
7317
7318	// Path-scoped cookie (uses DIR_PATH when available)
7319	$.cookie = function(key, value, expire) {
7320		return arguments.length > 1
7321			? _cookie(DIR_SCOPE, key, value, expire)
7322			: _cookie(DIR_SCOPE, key);
7323	};
7324
7325	// Global cookie (always scoped to '/')
7326	$.albumCookie = function(key, value, expire) {
7327		return arguments.length > 1
7328			? _cookie(GLOBAL_SCOPE, key, value, expire)
7329			: _cookie(GLOBAL_SCOPE, key);
7330	};
7331
7332})(jQuery);
7333/*	
7334 *	dropdownMenu() :: operates the dropdown menu
7335 *
7336 *	Copyright by Lazaworx
7337 *	http://www.lazaworx.com
7338 *	Author: Laszlo Molnar
7339 *
7340 *	Dual licensed under the MIT and GPL licenses.
7341 *	- http://www.opensource.org/licenses/mit-license.php
7342 *	- http://www.gnu.org/copyleft/gpl.html
7343 *
7344 *	Two modes, switched by settings.flyoutQuery:
7345 *		- accordion (narrow screens): submenus expand in place, max-height animation
7346 *		- flyout    (wide screens):   submenus are popovers, rendered in the top layer,
7347 *		                              so no ancestor overflow can ever clip them
7348 *
7349 *	Usage: $(element).dropdownMenu(options);
7350 *	       $(element).dropdownMenu('refresh');		// after the markup changed
7351 *	       $(element).dropdownMenu('close');
7352 *	       $(element).dropdownMenu('destroy');
7353 */
7354
7355;(function($) {
7356	'use strict';
7357
7358	var uidCnt = 0,
7359
7360		supportsPopover = (typeof HTMLElement !== 'undefined') &&
7361				Object.prototype.hasOwnProperty.call(HTMLElement.prototype, 'popover'),
7362
7363		supportsAnchor = !!(window.CSS && window.CSS.supports &&
7364				window.CSS.supports('anchor-name', '--x')),
7365
7366		getUniqueId = function(prefix) {
7367				uidCnt += 1;
7368				return (prefix || 'ldm') + '-' + uidCnt;
7369			},
7370
7371		// Get event coordinates. Returns null for keyboard-triggered clicks.
7372
7373		getCoords = function(e) {
7374				var oe = e.originalEvent || e,
7375					t = oe.touches || oe.changedTouches;
7376
7377				if (t && t.length) {
7378					return {
7379						x: t[0].clientX,
7380						y: t[0].clientY
7381					};
7382				}
7383
7384				// A click fired by Enter/Space reports 0,0 - treat it as "no pointer"
7385				if (typeof e.clientX === 'number' && (e.clientX || e.clientY)) {
7386					return {
7387						x: e.clientX,
7388						y: e.clientY
7389					};
7390				}
7391
7392				return null;
7393			};
7394
7395	$.fn.dropdownMenu = function(settings) {
7396
7397			// Method calls: $(el).dropdownMenu('refresh')
7398
7399			if (typeof settings === 'string') {
7400				var method = settings;
7401
7402				return this.each(function() {
7403						var api = $(this).data('dropdownMenu');
7404
7405						if (api && typeof api[method] === 'function') {
7406							api[method]();
7407						}
7408					});
7409			}
7410
7411			settings = $.extend({}, $.fn.dropdownMenu.defaults, settings);
7412
7413			return this.each(function() {
7414					var self = $(this),
7415						ns = '_ldm_' + (uidCnt += 1),
7416						mql = window.matchMedia(settings.flyoutQuery),
7417						useFlyout = null,				// null = not initialised yet
7418						cachedWidth = null,
7419						lastDate = new Date(),
7420						resizeTimer = null,
7421						hoverTimer = null,
7422						rafPending = false,
7423
7424						submenus = function() {
7425								return self.find('li.' + settings.hasDropdownClass + ' > ' + settings.submenuSelector);
7426							},
7427
7428						subOf = function(li) {
7429								return li.children(settings.submenuSelector);
7430							},
7431
7432						allMenus = function() {
7433								return self.find(settings.submenuSelector).addBack(settings.submenuSelector);
7434							},
7435
7436						triggerOf = function(li) {
7437								return li.children('a, button').first();
7438							},
7439
7440						openItems = function() {
7441								return self.find('li.' + settings.hasDropdownClass + '.' + settings.openMenuClass);
7442							},
7443
7444
7445						/* ----------------------------------------------------------
7446						 *	Height cache (accordion mode only)
7447						 *
7448						 *	Only the NATURAL height is cached - it depends on the
7449						 *	container width, the fonts and the DOM, nothing else.
7450						 *	The viewport clamp (settings.fixed) depends on the scroll
7451						 *	position and on which siblings happen to be open, so it is
7452						 *	recomputed on every open and never stored.
7453						 * -------------------------------------------------------- */
7454
7455						naturalHeight = function(menu) {
7456								var h = menu.data('ldmNat');
7457
7458								if (typeof h !== 'number') {
7459									// Not measured yet: lay it out invisibly
7460									var prev = {
7461											display:	menu[0].style.display,
7462											visibility:	menu[0].style.visibility,
7463											maxHeight:	menu[0].style.maxHeight
7464										};
7465
7466									menu.css({
7467											display:	'flex',
7468											visibility:	'hidden',
7469											maxHeight:	'none'
7470										});
7471
7472									h = menu.outerHeight();
7473
7474									menu.css(prev);
7475									menu.data('ldmNat', h);
7476								}
7477
7478								return h;
7479							},
7480
7481						openHeight = function(menu) {
7482								var h = naturalHeight(menu),
7483									t;
7484
7485								if (settings.fixed) {
7486									t = menu[0].getBoundingClientRect();
7487									h = Math.min(window.innerHeight - t.top, h);
7488								}
7489
7490								return h;
7491							},
7492
7493						invalidateHeights = function() {
7494								allMenus().removeData('ldmNat');
7495							},
7496
7497
7498						/* ----------------------------------------------------------
7499						 *	Accordion mode
7500						 * -------------------------------------------------------- */
7501
7502						doAnimations = function(el, close) {
7503								var toHide = $(),
7504									toShow = $(),
7505									hasSubmenu = false,
7506									p = el.parents('.' + settings.openMenuClass);	// Parent(s): do not hide them
7507
7508								if (close) {
7509									p = p.slice(1);
7510								}
7511
7512								self.find('.' + settings.openMenuClass).not(p).each(function() {
7513										var menu = subOf($(this));
7514
7515										if (menu.length) {
7516											menu.css('maxHeight', openHeight(menu) + 'px');
7517											toHide = toHide.add(menu);
7518										}
7519
7520										$(this).removeClass(settings.openMenuClass);
7521										triggerOf($(this)).attr('aria-expanded', 'false');
7522									});
7523
7524								toHide.one('transitionend.' + ns, function() {
7525										$(this).css({
7526												display:	'none',
7527												transition:	'none'
7528											});
7529									})
7530									.css({
7531											overflow:	'hidden'
7532										});
7533
7534								if (!close) {
7535									p = el.parent();
7536
7537									if (!p.hasClass(settings.openMenuClass)) {
7538										toShow = subOf(p);
7539
7540										if (toShow.length) {
7541											// Force a measurement now, while the menu is still collapsed
7542											naturalHeight(toShow);
7543											hasSubmenu = toShow.children('.' + settings.hasDropdownClass).length > 0;
7544										}
7545
7546										p.addClass(settings.openMenuClass);
7547										el.attr('aria-expanded', 'true');
7548									}
7549
7550									toShow.parents(settings.submenuSelector).css({
7551											maxHeight:	'none'
7552										});
7553
7554									toShow.on('transitionend.' + ns, function() {
7555											$(this).css({
7556													display:	'flex',
7557													overflow:	hasSubmenu ? 'visible' : 'auto',
7558													maxHeight:	'none'
7559												});
7560										})
7561										.css({
7562											maxHeight:	0,
7563											display:	'flex',
7564											overflow:	'hidden',
7565											visibility:	'visible'
7566										});
7567								}
7568
7569								window.requestAnimationFrame(function() {
7570										toHide.css({
7571												transition:	'max-height ' + settings.duration + 'ms ease-out',
7572												maxHeight:	0
7573											});
7574
7575										toShow.css({
7576												transition:	'max-height ' + settings.duration + 'ms ease-out',
7577												maxHeight:	openHeight(toShow) + 'px'
7578											});
7579									});
7580							},
7581
7582
7583						/* ----------------------------------------------------------
7584						 *	Flyout mode (popover)
7585						 * -------------------------------------------------------- */
7586
7587						isTopLevel = function(li) {
7588								return li.parent().closest('li.' + settings.hasDropdownClass).length === 0;
7589							},
7590
7591						placeFlyout = function(menu, li) {
7592								// Fallback only: used when CSS anchor positioning is unavailable
7593								var trigger = triggerOf(li)[0] || li[0],
7594									r = trigger.getBoundingClientRect(),
7595									pad = settings.viewportPadding,
7596									top = isTopLevel(li),
7597									w, h, x, y;
7598
7599								$(menu).css({
7600										position:	'fixed',
7601										left:		'0px',
7602										top:		'0px',
7603										maxHeight:	(window.innerHeight - 2 * pad) + 'px'
7604									});
7605
7606								w = menu.offsetWidth;
7607								h = menu.offsetHeight;
7608
7609								x = top ? r.left : r.right;
7610								y = top ? r.bottom : r.top;
7611
7612								// Flip horizontally if it would leave the viewport
7613								if (x + w > window.innerWidth - pad) {
7614									x = (top ? r.right : r.left) - w;
7615								}
7616								x = Math.max(pad, Math.min(x, window.innerWidth - w - pad));
7617
7618								// Shift up if it would leave the viewport
7619								if (y + h > window.innerHeight - pad) {
7620									y = Math.max(pad, window.innerHeight - h - pad);
7621								}
7622
7623								$(menu).css({
7624										left:	Math.round(x) + 'px',
7625										top:	Math.round(y) + 'px'
7626									});
7627							},
7628
7629						repositionOpen = function() {
7630								if (supportsAnchor) {
7631									return;						// the browser tracks the anchor for us
7632								}
7633
7634								openItems().each(function() {
7635										var li = $(this),
7636											menu = subOf(li)[0];
7637
7638										if (menu && menu.matches(':popover-open')) {
7639											placeFlyout(menu, li);
7640										}
7641									});
7642							},
7643
7644						showFlyout = function(li) {
7645								var menu = subOf(li)[0];
7646
7647								closeFlyoutsExcept(li);
7648
7649								if (!menu || menu.matches(':popover-open')) {
7650									return;
7651								}
7652
7653								try {
7654									menu.showPopover();
7655								} catch (err) {
7656									return;
7657								}
7658
7659								li.addClass(settings.openMenuClass);
7660								triggerOf(li).attr('aria-expanded', 'true');
7661
7662								if (!supportsAnchor) {
7663									placeFlyout(menu, li);
7664								}
7665							},
7666
7667						hideFlyout = function(li) {
7668								var menu = subOf(li)[0];
7669
7670								// Close descendants first: a manual popover does not close its
7671								// nested popovers, and hiding the parent would leave them stranded
7672								subOf(li)
7673									.children('.' + settings.hasDropdownClass + '.' + settings.openMenuClass)
7674									.each(function() {
7675											hideFlyout($(this));
7676										});
7677
7678								if (menu && menu.matches(':popover-open')) {
7679									try {
7680										menu.hidePopover();
7681									} catch (err) {
7682										// already hidden
7683									}
7684								}
7685
7686								li.removeClass(settings.openMenuClass);
7687								triggerOf(li).attr('aria-expanded', 'false');
7688							},
7689
7690						closeFlyoutsExcept = function(li) {
7691								var keep = li ? li.parents('li.' + settings.hasDropdownClass).add(li) : $();
7692								
7693								openItems().not(keep).each(function() {
7694										if (li && $(this).find(li).length) {
7695											return;
7696										}
7697										hideFlyout($(this));
7698									});
7699							},
7700
7701
7702						/* ----------------------------------------------------------
7703						 *	Mode switching
7704						 * -------------------------------------------------------- */
7705
7706						enablePopovers = function() {
7707								submenus().each(function() {
7708										var menu = this,
7709											$menu = $(menu),
7710											li = $menu.parent(),
7711											trigger = triggerOf(li)[0],
7712											id = menu.id || (menu.id = getUniqueId('ldm-menu')),
7713											name;
7714
7715										menu.setAttribute('popover', 'manual');
7716										$menu.addClass(settings.flyoutClass);
7717
7718										if (supportsAnchor && trigger) {
7719											name = '--' + id;
7720											trigger.style.anchorName = name;
7721											menu.style.positionAnchor = name;
7722										}
7723
7724										if (trigger) {
7725											trigger.setAttribute('aria-haspopup', 'true');
7726											trigger.setAttribute('aria-expanded', 'false');
7727											trigger.setAttribute('aria-controls', id);
7728										}
7729									});
7730							},
7731
7732						disablePopovers = function() {
7733								submenus().each(function() {
7734										if (this.matches(':popover-open')) {
7735											try {
7736												this.hidePopover();
7737											} catch (err) {
7738												// ignore
7739											}
7740										}
7741
7742										this.removeAttribute('popover');
7743										this.style.positionAnchor = '';
7744										this.style.position = '';
7745										this.style.left = '';
7746										this.style.top = '';
7747										this.style.maxHeight = '';
7748
7749										$(this).removeClass(settings.flyoutClass)
7750											.parent()
7751											.children('a, button')
7752											.each(function() {
7753													this.style.anchorName = '';
7754												});
7755									});
7756							},
7757
7758						closeAll = function() {
7759								if (useFlyout) {
7760									closeFlyoutsExcept(null);
7761								} else {
7762									openItems().each(function() {
7763											$(this).removeClass(settings.openMenuClass);
7764											triggerOf($(this)).attr('aria-expanded', 'false');
7765										});
7766
7767									allMenus().css({
7768											display:	'',
7769											maxHeight:	'',
7770											overflow:	'',
7771											transition:	'',
7772											visibility:	''
7773										});
7774								}
7775							},
7776
7777						setMode = function(flyout) {
7778								flyout = flyout && supportsPopover;
7779
7780								if (flyout === useFlyout) {
7781									return;
7782								}
7783
7784								if (useFlyout !== null) {
7785									closeAll();
7786								}
7787
7788								useFlyout = flyout;
7789
7790								if (useFlyout) {
7791									allMenus().css({
7792											display:	'',
7793											maxHeight:	'',
7794											overflow:	'',
7795											transition:	'',
7796											visibility:	''
7797										});
7798									enablePopovers();
7799								} else {
7800									disablePopovers();
7801								}
7802
7803								self.toggleClass(settings.flyoutModeClass, useFlyout);
7804							},
7805
7806
7807						/* ----------------------------------------------------------
7808						 *	Cache invalidation
7809						 * -------------------------------------------------------- */
7810
7811						onResize = function() {
7812								window.clearTimeout(resizeTimer);
7813
7814								resizeTimer = window.setTimeout(function() {
7815										var w = window.innerWidth;
7816
7817										if (w !== cachedWidth) {
7818											cachedWidth = w;
7819											invalidateHeights();
7820											closeAll();				// inline max-heights are stale now
7821										}
7822
7823										setMode(mql.matches);
7824										repositionOpen();
7825									}, settings.resizeDelay);
7826							},
7827
7828						onScroll = function() {
7829								if (rafPending || !useFlyout || supportsAnchor) {
7830									return;
7831								}
7832
7833								rafPending = true;
7834
7835								window.requestAnimationFrame(function() {
7836										rafPending = false;
7837										repositionOpen();
7838									});
7839							},
7840
7841						refresh = function() {
7842								invalidateHeights();
7843								closeAll();
7844
7845								if (useFlyout) {
7846									enablePopovers();		// pick up newly added submenus
7847								}
7848							},
7849
7850						destroy = function() {
7851								window.clearTimeout(resizeTimer);
7852								window.clearTimeout(hoverTimer);
7853								closeAll();
7854								disablePopovers();
7855
7856								self.off('.' + ns);
7857								$(window).off('.' + ns);
7858								$(document).off('.' + ns);
7859								self.removeData('dropdownMenu');
7860							};
7861
7862
7863					/* --------------------------------------------------------------
7864					 *	Bindings
7865					 * ------------------------------------------------------------ */
7866
7867					cachedWidth = window.innerWidth;
7868					setMode(mql.matches);
7869
7870					self.on('click.' + ns, 'li.' + settings.hasDropdownClass + ' > a, li.' + settings.hasDropdownClass + ' > button', function(e) {
7871							var el = $(this),
7872								li = el.parent(),
7873								open = li.hasClass(settings.openMenuClass),
7874								coords,
7875								arrowClick,
7876								hasDestination;
7877
7878							if (useFlyout) {
7879
7880								hasDestination = el.is('a') && el.attr('href') && el.attr('href') !== '#';
7881
7882								coords = getCoords(e);
7883								arrowClick = coords ?
7884										(coords.x - el.offset().left) > (el.outerWidth() - settings.arrowZone) :
7885										false;						// keyboard here means Enter on a focused link: let it navigate
7886
7887								if (hasDestination && !arrowClick) {
7888									closeFlyoutsExcept(null);		// tidy up before leaving the page
7889									return true;					// let the browser follow the href
7890								}
7891
7892								if (open) {
7893									hideFlyout(li);
7894								} else {
7895									showFlyout(li);
7896								}
7897
7898								e.stopPropagation();
7899								e.preventDefault();
7900								return false;
7901							}
7902
7903							// Accordion mode
7904							if ((new Date() - lastDate) <= settings.tapDelay) {
7905								return true;
7906							}
7907
7908							coords = getCoords(e);
7909							arrowClick = coords ?
7910									(coords.x - el.offset().left) > (el.outerWidth() - settings.arrowZone) :
7911									true;			// keyboard: always treat as a toggle
7912
7913							if (!open || arrowClick) {
7914								e.stopImmediatePropagation();
7915								lastDate = new Date();
7916
7917								window.requestAnimationFrame(function() {
7918										doAnimations(el, arrowClick && open);
7919									});
7920
7921								return false;
7922							}
7923
7924							return true;
7925						});
7926
7927					if (settings.hover) {
7928						self.on('mouseenter.' + ns, 'li.' + settings.hasDropdownClass, function() {
7929								var li = $(this);
7930
7931								if (!useFlyout) {
7932									return;
7933								}
7934
7935								window.clearTimeout(hoverTimer);
7936								closeFlyoutsExcept(li);
7937								hoverTimer = window.setTimeout(function() {
7938										showFlyout(li);
7939									}, settings.hoverDelay);
7940							})
7941							.on('mouseleave.' + ns, function() {
7942								if (useFlyout) {
7943									window.clearTimeout(hoverTimer);
7944									hoverTimer = window.setTimeout(function() {
7945											closeFlyoutsExcept(null);
7946										}, settings.closeDelay);
7947								}
7948							});
7949					}
7950
7951					// popover="manual" means we own dismissal
7952					$(document).on('pointerdown.' + ns, function(e) {
7953							if (useFlyout && !$(e.target).closest(self).length &&
7954									!$(e.target).closest('.' + settings.flyoutClass).length) {
7955								closeFlyoutsExcept(null);
7956							}
7957						})
7958						.on('keydown.' + ns, function(e) {
7959							if (useFlyout && e.key === 'Escape') {
7960								var deepest = openItems().last();
7961
7962								if (deepest.length) {
7963									hideFlyout(deepest);
7964									triggerOf(deepest).trigger('focus');
7965									e.preventDefault();
7966								}
7967							}
7968						});
7969
7970					$(window).on('resize.' + ns + ' orientationchange.' + ns, onResize)
7971						.on('scroll.' + ns, onScroll);
7972
7973					// Web fonts change line heights: everything measured before they
7974					// land is a lie
7975					if (document.fonts && document.fonts.ready && document.fonts.ready.then) {
7976						document.fonts.ready.then(function() {
7977								invalidateHeights();
7978							});
7979					}
7980
7981					self.data('dropdownMenu', {
7982							refresh:	refresh,
7983							close:		closeAll,
7984							destroy:	destroy
7985						});
7986				});
7987		};
7988
7989	$.fn.dropdownMenu.defaults = {
7990			hasDropdownClass:	'has-submenu',
7991			menuClass:			'menu',					// the root <ul> only
7992			submenuSelector:	'ul',					// a submenu is simply the li's child list
7993			openMenuClass:		'opened',
7994			flyoutClass:		'is-flyout',			// added to every popover submenu
7995			flyoutModeClass:	'flyout-mode',			// added to the root in flyout mode
7996			flyoutQuery:		'(min-width: 768px)',	// below this: accordion
7997			fixed:				false,					// accordion: clamp to the viewport
7998			hover:				true,					// flyout: open on hover
7999			hoverDelay:			120,
8000			closeDelay:			2000,					// grace period before mouseleave closes a flyout
8001			arrowZone:			32,
8002			viewportPadding:	8,
8003			duration:			500,
8004			tapDelay:			300,
8005			resizeDelay:		150
8006		};
8007
8008})(jQuery);
8009;(function($, $window) {
8010	'use strict';
8011				
8012	/* 
8013		scrollTopBtn.js - add a "scroll to top" button
8014	*/
8015
8016	$.fn.scrollToTopBtn = function() {
8017		
8018			var self =	$(this).eq(0),
8019				ns =	'lst_' + ($.fn.scrollToTopBtn._counter = ($.fn.scrollToTopBtn._counter || 0) + 1),
8020				text =	getTranslations({
8021							scrollTopTooltip:				"Top"
8022						}),
8023			
8024				btn = $('<a>', {
8025							'class': 		'scrollup icon-arrow-up',
8026							role:			'button',
8027							title:			text.scrollTopTooltip
8028						}).appendTo(self);
8029			
8030			// Show/hide based on scroll position
8031			$window.on('scroll.' + ns, function() { 
8032					btn.toggleClass('show', $window.scrollTop() > 16);
8033					return true;
8034				});
8035			
8036			btn.on('click.' + ns, function() {
8037					if ($(this).hasClass('show')) {
8038						// Scroll html or body, whichever is the scroll root
8039						var target = $('html').scrollTop() ? $('html') : $('body');
8040						
8041						target.animate({ 
8042								scrollTop: 	0 
8043							}, 500);
8044						
8045						return false;
8046					}
8047				});
8048			
8049			btn.addTooltip({
8050					pos: [ 2, 1, 0, 1 ]
8051				});
8052
8053			// Allow external teardown
8054			self.on('removeScrollToTopBtn.' + ns, function() {
8055					$window.off('.' + ns);
8056					self.off('.' + ns);
8057					btn.remove();
8058				});
8059			
8060			return this;
8061		};
8062	
8063}(jQuery, jQuery(window)));
8064/*	
8065 *	alignTo() :: align a layer to another
8066 *
8067 *	Copyright by Lazaworx
8068 *	http://www.lazaworx.com
8069 *	Author: Laszlo Molnar
8070 *
8071 *	Dual licensed under the MIT and GPL licenses.
8072 *	- http://www.opensource.org/licenses/mit-license.php
8073 *	- http://www.gnu.org/copyleft/gpl.html
8074 *
8075 *	Usage: $(element).alignTo(target, options);
8076 *	options: gap, posX, posY, toX, toY
8077 */
8078
8079;(function($, $window) {
8080	'use strict';
8081	
8082	var ALIGN_LEFT = 0,  
8083		ALIGN_TOP = 0,
8084		ALIGN_CENTER = 1, 
8085		ALIGN_MIDDLE = 1,
8086		ALIGN_RIGHT = 2, 
8087		ALIGN_BOTTOM = 2,
8088		
8089		getFixedParent = function(el) {
8090				
8091				while (el.length && el[0].nodeName !== 'BODY') {
8092					if (el.css('position') === 'fixed') {
8093						return el;
8094					}
8095					el = el.parent();
8096				}
8097				
8098				return $();
8099			};
8100	
8101	$.fn.alignTo = function(target, settings) {
8102		
8103			settings = $.extend({}, $.fn.alignTo.defaults, settings);
8104			
8105			if (typeof target === 'string') {
8106				target = $(target);
8107			}
8108			
8109			if (!(target instanceof $ && target.length)) {
8110				return;
8111			}
8112				
8113			var to, 
8114				tw, 
8115				th,
8116				ww = $(window).width(),
8117				wh = $(window).height(),
8118				fixedParent = getFixedParent(target);
8119				
8120			if (target[0].nodeName === 'AREA') {
8121				var shape = target[0].shape.toUpperCase();
8122				if (shape === 'RECT' || shape === 'CIRCLE' || shape === 'POLY') {
8123					var r = target[0].coords.split(',');
8124					to = target.parent().offset();
8125					// Area left, top
8126					to.left += parseInt(r[0],10);
8127					to.top += parseInt(r[1],10);
8128					// Area width, height
8129					tw = parseInt(r[2]||0,10) - parseInt(r[0],10);
8130					th = parseInt(r[3]||0,10) - parseInt(r[1],10);
8131				}
8132			} else {
8133				// Normal element
8134				to = target.offset();
8135				tw = target.outerWidth();
8136				th = target.outerHeight();
8137			}
8138			
8139			to.left -= $window.scrollLeft();
8140			to.top -= $window.scrollTop();
8141						
8142			return this.each(function() {
8143					var e = $(this),
8144						nub = e.find('.' + settings.nubClass),
8145						nubPos,
8146						secPos,
8147						hal = settings.pos[2], 
8148						val = settings.pos[3];
8149						
8150					e.css('maxHeight', 'none');
8151					
8152					var	w = e.outerWidth(),
8153						h = e.outerHeight(),
8154						rx = Math.round(to.left + settings.pos[2] * tw / 2 + 
8155							(settings.pos[1] === ALIGN_MIDDLE? ((settings.pos[2] - 1) * settings.gap) : ((settings.pos[0] - settings.pos[2]) * settings.nubSize))),
8156						ry = Math.round(to.top + settings.pos[3] * th / 2 + 
8157							(settings.pos[1] !== ALIGN_MIDDLE? ((settings.pos[3] - 1) * settings.gap) : 0)),
8158						l = Math.round(rx - settings.pos[0] * w / 2),
8159						t = Math.round(ry - settings.pos[1] * h / 2);
8160					
8161					if (t < 0 || (t + h) > wh) {
8162						// Overflow - vertical
8163						if (settings.pos[2] !== ALIGN_CENTER) {
8164							// Aligned to sides, just make sure it won't hang above
8165							t = ((2 * t + h) > wh)? (wh - h) : 0;
8166						} else if (settings.pos[3] === ALIGN_TOP) {
8167							if (wh > (to.top * 2 + th)) {
8168								// More space below :: moving below
8169								t = to.top + th + settings.gap;
8170								val = ALIGN_BOTTOM;
8171							}
8172						} else if (settings.pos[3] === ALIGN_BOTTOM) { 
8173							if (wh < (to.top * 2 + th)) {
8174								// More space above :: move above
8175								t = Math.max(0, to.top - h - settings.gap);
8176								val = ALIGN_TOP;
8177							}
8178						}
8179						if (t < 0) {
8180							t = 0;
8181						}
8182					}
8183								
8184					if (l < 0 || (l + w) > ww) {
8185						// Overflow - horizontal
8186						if (settings.pos[3] !== ALIGN_MIDDLE) {
8187							// Not aligned to vertical center
8188							l = ((2 * l + w) > ww)? (ww - w) : 0;
8189						} else if (settings.pos[2] === ALIGN_LEFT) {
8190							if (ww > (to.left * 2 + tw)) {
8191								// More space right :: move right
8192								l = to.left + tw + settings.gap;
8193								hal = ALIGN_RIGHT;
8194							}
8195						} else if (settings.pos[2] === ALIGN_RIGHT) { 
8196							if (ww < (to.left * 2 + tw)) {
8197								// More space left :: move left
8198								l = Math.max(0, to.left - w - settings.gap);
8199								hal = ALIGN_LEFT;
8200							}
8201						}
8202						if (l < 0) {
8203							l = 0;
8204						}
8205						if ((l + w) > ww) {
8206							// Still oversized
8207							e.css({
8208								//overflow: 	'auto',
8209								maxWidth: 	ww - l - (parseInt(e.css('paddingLeft'), 10) + parseInt(e.css('paddingRight'), 10) + parseInt(e.css('borderLeftWidth'), 10) + parseInt(e.css('borderRightWidth'), 10))
8210							});
8211						}
8212					} 
8213					
8214					if (settings.fixNub && nub.length && (hal !== ALIGN_CENTER || val !== ALIGN_MIDDLE)) {
8215						var css = {
8216									left:		'auto',
8217									right:		'auto',
8218									top:		'auto',
8219									bottom:		'auto'
8220								};
8221						
8222						if (settings.pos[1] !== ALIGN_MIDDLE) {
8223							// Align nub horizontally
8224							css.left = to.left - l + hal * tw / 2 - hal * settings.nubSize;
8225							if (val === ALIGN_TOP) {
8226								css.bottom = - 2 * settings.nubSize;
8227								nubPos = 'top';
8228							} else {
8229								css.top = - 2 * settings.nubSize;
8230								nubPos = 'bottom';
8231							}
8232							secPos = (settings.pos[0] === ALIGN_LEFT)? 'left' : ((settings.pos[0] === ALIGN_RIGHT)? 'right' : 'center');
8233						} else {
8234							// Align nub vertically
8235							css.top = to.top - t + val * th / 2 - val * settings.nubSize;
8236							if (hal === ALIGN_LEFT) {
8237								css.right = - 2 * settings.nubSize;
8238								nubPos = 'left';
8239							} else {
8240								css.left = - 2 * settings.nubSize;
8241								nubPos = 'right';
8242							}
8243							secPos = (settings.pos[1] === ALIGN_TOP)? 'top' : ((settings.pos[1] === ALIGN_BOTTOM)? 'bottom' : 'middle');
8244						}
8245						
8246						nub.css(css);
8247					}
8248					
8249					if (fixedParent.length) {
8250						e.css({
8251								position: 	'fixed',
8252								left: 		l, 
8253								top: 		t 
8254							});
8255					} else {
8256						e.css({
8257								position: 	'absolute',
8258								left: 		l + $(window).scrollLeft(),
8259								top: 		t + $(window).scrollTop() 
8260							});
8261					}
8262					
8263					e.removeClass('align-left align-right align-top align-bottom').removeClass('sec-left sec-right sec-top sec-bottom sec-center sec-middle');
8264
8265					if (nubPos) {
8266						e.addClass('align-' + nubPos);
8267					}
8268					if (secPos) {
8269						e.addClass('sec-' + secPos);
8270					}
8271					
8272				});
8273		};
8274
8275	$.fn.alignTo.defaults = {
8276			gap: 			0,
8277			pos: 			[ ALIGN_CENTER, ALIGN_BOTTOM, ALIGN_CENTER, ALIGN_TOP ],
8278			fixNub:			true,
8279			nubClass:		'nub',
8280			nubSize:		5
8281		};
8282
8283})(jQuery, jQuery(window));
8284/*	
8285 *	addTooltip() :: little Popup displaying 'title' text, or passed text (can be HTML)
8286 *
8287 *	Copyright by Lazaworx
8288 *	http://www.lazaworx.com
8289 *	Author: Laszlo Molnar
8290 *
8291 *	Dual licensed under the MIT and GPL licenses.
8292 *	- http://www.opensource.org/licenses/mit-license.php
8293 *	- http://www.gnu.org/copyleft/gpl.html
8294 *
8295 *	Usage: $(element).addTooltip( [txt,] options );
8296 *	options:
8297		id: 	'tooltip',
8298		stay: 	3000,
8299		posX: 	ALIGN_CENTER,
8300		posY: 	ALIGN_BOTTOM,
8301		toX: 	ALIGN_CENTER,
8302		toY: 	ALIGN_TOP
8303 */
8304
8305;(function($) {
8306	'use strict';
8307			
8308	$.fn.hideAllTooltips = function(except) {
8309		
8310			return this.each(function() {
8311					
8312					//log('Hiding all tooltips');
8313					$(this).data('suppressTooltips', true).find('[data-tooltip-id]').each(function() {
8314							if (!$(this).data('tooltip-keep')) { 
8315								var id = $(this).attr('data-tooltip-id');
8316								
8317								if (id && id.length && id !== except) {
8318									$('#' + id).hide();
8319								}
8320							}
8321						});
8322
8323					$(this).data('suppressTooltips', false);  // restore
8324				});
8325		};
8326	
8327	$.fn.destroyAllTooltips = function(except) {
8328		
8329			return this.each(function() {
8330					
8331					//log('Removing ' + $(this).find('[data-tooltip-id]').length + ' tooltip(s).');
8332					$(this).data('suppressTooltips', true).find('[data-tooltip-id]').each(function() {
8333							var id = $(this).attr('data-tooltip-id');
8334							//log(id);
8335							if (id && id.length && id !== except) {
8336								$('#' + id).remove();
8337							}
8338						});
8339				});
8340		};
8341	
8342	// Hiding all tooltips on mouse leaving the document
8343	$(document).on('mouseleave._ltt_', function() {
8344			$(document.body).hideAllTooltips();
8345		});
8346	
8347	$.fn.addTooltip = function(content, settings) {
8348		
8349			if (typeof content !== UNDEF && typeof content !== STRING && !(content && content.jquery)) {
8350				settings = content;
8351				content = null;
8352			}
8353			
8354			settings = $.extend({}, $.fn.addTooltip.defaults, settings);
8355			
8356			//console.log(content || $(this).data('tooltip') || $(this).attr('title'));
8357			
8358			var isVisible = function(el) {
8359						
8360						if (typeof el === UNDEF || !el || !el.length) {
8361							return true;
8362						} else if (el.is(':hidden') || el.css('opacity') < .25) {
8363							//console.log(el[0].id + ' is hidden or transparent!');
8364							return false;
8365						} else {
8366							var visible = true;
8367							el.parents().each(function() {
8368									if ($(this).is(':hidden') || $(this).css('opacity') < 0.25) {
8369										visible = false;
8370										return false;
8371									}
8372								});
8373							
8374							return visible;
8375						}
8376					},
8377					
8378				// Creating new tooltip
8379				createNew = function(el, cont) {
8380						var tt;
8381										
8382						if (!cont) {
8383							
8384							if (cont = el.data('tooltip')) {
8385								
8386								if (cont.jquery) {
8387									// jQuery element
8388									cont.removeClass('hidden');
8389								} else {
8390									
8391									if (typeof cont === STRING) {
8392										// Selector
8393										if (cont.charAt(0) === '.') {
8394											// read from related layer
8395											cont = el.find(cont).eq(0);
8396										} else if (cont.charAt(0) === '#') {
8397											// ID
8398											cont = $(cont);
8399										}
8400												
8401									} else {
8402										// Read settings from data
8403										if (typeof cont === OBJECT) {
8404											settings = $.extend({}, settings, cont);
8405										}
8406										
8407										if (settings.hasOwnProperty('html')) {
8408											// read from settings.html
8409											cont = settings['html'].replaceAll('\uff02', '"');
8410										} else {
8411											// read from title attr
8412											cont = el.attr('title');
8413											el.removeAttr('title');
8414										}
8415									}
8416								}
8417								
8418							} else {
8419								// read from data or title attr
8420								cont = el.attr('title');
8421								el.removeAttr('title');
8422							}
8423							
8424							if (!cont || !cont.length) {
8425								return $();
8426							}
8427							
8428							tt = $('<div>', {
8429									html: 	cont
8430								}).appendTo('body');
8431							
8432						} else if (typeof cont === STRING) {
8433							
8434							// passed directly :: html structure as string
8435							tt = $('<div>', {
8436									html: 	cont
8437								}).appendTo('body');
8438							
8439						} else if (cont.jquery) {
8440							
8441							// jQuery element
8442							if (!$.contains(document.body, cont[0])) {
8443								tt = cont.appendTo('body');
8444							} else {
8445								tt = cont;
8446							}
8447							
8448						} else {
8449							return $();
8450						}
8451						
8452						if (tt.length === 0) {
8453							return $();
8454						}
8455						
8456						tt	.attr('id', el.attr('data-tooltip-id'))
8457							.addClass(settings.className)
8458							.attr('role', 'tooltip')
8459							.append($('<span>', {
8460								'class': 	settings.nub
8461							}))
8462							.data('display', (tt.css('display') === 'none')? 'block' : tt.css('display'))
8463							.attr('aria-hidden', 'true')
8464							.hide();
8465						
8466						return tt;
8467						
8468					};
8469			
8470			return this.each(function() {
8471				
8472					if ($(this).data('tooltip-id')) {
8473						// already has tooltip
8474						return true;
8475					}
8476					
8477					var self = $(this), 	// trigger element
8478						options = settings,	// initializing with outer scope settings 
8479						tt,					// tooltip layer
8480						to, 				// show timeout
8481						hto,				// hide timeout
8482						overtt = false,		// over the tooltip 
8483						focus = false,		// tooltip input got focus
8484						offs,				// last offset to detect if moving
8485						start,				// event start
8486						wasOn = false,		// was on at the beginning of the event?
8487						ns = '_ltt_' + ($.fn.addTooltip._counter = ($.fn.addTooltip._counter || 0) + 1),
8488							
8489						// Create
8490						create = function() {
8491								//log('Creating tooltip');
8492								if (self.data('suppressTooltips') || !(tt = createNew(self, content))) {
8493									return false;
8494								}
8495								
8496								// Keep the popup live while the mouse is over
8497								tt	.on('mouseenter.' + ns, getFocus)
8498									.on('mouseleave.' + ns, lostFocus);
8499										
8500								// ... or an input box has focus
8501								tt	.find('input, textarea')
8502									.on('focus.' + ns, function() {
8503											focus = true;
8504											getFocus(this);
8505										})
8506									.on('blur.' + ns, function() {
8507											focus = false;
8508										});
8509									
8510								// A button that closes the tooltip on click
8511								tt	.find('[data-closer]')
8512									.on('click.' + ns, function() {
8513											setTimeout(hideLater, 1000);
8514											return true;
8515										});
8516									
8517								return true;
8518							},
8519						
8520						// getFocus
8521						getFocus =  function(e) {
8522								
8523								//log('Getting focus (' + e.type + ')');
8524								overtt = true;
8525								if (isVisible($(this))) {
8526									clearTimeout(hto);
8527								}
8528							},
8529						
8530						// lostFocus
8531						lostFocus = function(e) {
8532		
8533								//log('Losing focus (' + e.type + ')');
8534								if (focus) {
8535									return;
8536								}
8537								
8538								overtt = false;
8539								clearTimeout(hto);
8540								hto = setTimeout(hide, Math.max(options.stay, 500));
8541							},
8542							
8543						// Hiding the popup
8544						hide = function() {
8545								
8546								//log('Hiding tooltip (over = ' + overtt + ')');
8547								clearTimeout(hto);
8548								
8549								if (overtt === false) {
8550									wasOn = false; 
8551									clearTimeout(to);
8552									//events = '';
8553									if (tt) {
8554										tt	.css({
8555													opacity:	0
8556												})
8557											.one('transitionend', function() {
8558													$(this).css({
8559															display:	'none'
8560														});
8561												});
8562									}
8563								}
8564							},
8565						
8566						// Hide later :: automatically on touch devices
8567						hideLater = function() {
8568								//log('Hiding tooltip (stay = ' + options.stay + 'ms)');
8569								clearTimeout(hto);
8570								hto = setTimeout(hide, options.stay);
8571							},
8572						
8573						
8574						// Showing, aligning and fading in
8575						show = function() {
8576								var o = self.offset();
8577								//log('Showing tooltip');
8578								if (self.data('suppressTooltips')) {
8579									return;
8580								}
8581								
8582								clearTimeout(hto);
8583								//log('pos: ' + o.left.toFixed(3) + ',' + o.top.toFixed(3));
8584								
8585								if (!offs || (Math.abs(o.top - offs.top) < 1 && Math.abs(o.left - offs.left) < 1)) {
8586									// only if target layer not in move
8587									if (options.exclusive) {
8588										self.data('tooltip-keep', true);
8589										$(document.body).hideAllTooltips(self.data('tooltip-id'));
8590										self.data('tooltip-keep', null);
8591									}
8592									
8593									tt	.css({
8594												opacity: 	0,
8595												display:	'block'
8596											})
8597										.alignTo(self, {
8598												gap: 		options.gap,
8599												pos: 		options.pos
8600											})
8601										.css({
8602												opacity: 	1
8603											})
8604										.one('transitionend', function() {
8605												$(this).css({
8606														display:	'block'
8607													});
8608												hideLater();
8609											});
8610
8611									// Fallback if no transition is defined
8612									setTimeout(hideLater, 50);
8613
8614									wasOn = true;
8615										
8616								}
8617								
8618								offs = o;
8619							},
8620						
8621						// Entering the hotspot :: hover or focus
8622						enter = function(e) {
8623								//log('Entering spot (' + e.type + ')');
8624		
8625								if (!self.data('suppressTooltips') && isVisible($(e.target).closest('[data-tooltip-id]'))) {
8626									wasOn = ttVisible();
8627									start = new Date();
8628									offs = self.offset();
8629									tt = $('#' + self.data('tooltip-id'));
8630									//log('pos: ' + offs.left.toFixed(3) + ',' + offs.top.toFixed(3));
8631									
8632									if (!tt.length) {
8633										if (!create()) {
8634											destroy();
8635											return true;
8636										}
8637									} else {
8638										clearTimeout(hto);
8639									}
8640									
8641									clearTimeout(to);
8642									to = null;
8643									
8644									if (whatInput.ask('intent') === 'mouse') {
8645										to = setTimeout(show, options.delay);
8646									}
8647								}
8648								
8649								return true;
8650							},
8651						
8652						// Leaving the trigger element
8653						leave = function(e) {
8654								
8655								clearTimeout(to);
8656								to = null;
8657								
8658								if (isVisible($(e.target).closest('[data-tooltip-id]'))) {
8659									clearTimeout(hto);
8660									//log('Leaving spot (' + e.target.nodeName + ')');
8661									if (whatInput.ask('intent') === 'mouse') {
8662										hto = setTimeout(hide, 500);
8663									} else {
8664										hto = setTimeout(hide, 3000);
8665									}
8666								}
8667								
8668								return false;
8669							},
8670						
8671						// Avoid Click
8672						avoidClick = function(e) {
8673								e.preventDefault();
8674								clearTimeout(to);
8675								clearTimeout(hto);
8676							},
8677						
8678						ttVisible = function() {
8679								return !!tt && tt.is(':visible') && (tt.css('opacity') > 0.99);
8680							},
8681						
8682						// Test for link
8683						hasLink = function(el) {
8684								var	a = el.closest('a');
8685								return a.length && a.attr('href') && !a.attr('href').startsWith('javascript') && a.attr('href') !== '#';
8686							},
8687						
8688						// The hotspot clicked
8689						clicked = function(e) {
8690								//log('Clicking spot + (target: ' + e.target.nodeName + ')');
8691								//log(whatInput.ask());
8692								//log(events);
8693								
8694								if (isVisible($(e.target).closest('[data-tooltip-id]'))) {
8695									clearTimeout(to);
8696									
8697									if (options.touchToggle || whatInput.ask('intent') !== 'mouse') {
8698									//if (events.indexOf(TOUCH.START) !== -1 || isFireFox) {
8699										// touched
8700										// Firefox by default emulates touch events with mouse events, 
8701										// no way you can tell the difference, so it's safer to treat like touch
8702										
8703										var now = new Date();
8704										
8705										if (options.touchToggle || (now - start) > 1000) {
8706											// touch toggle or long touch
8707											//log('Touched for ' + (now-start) + 'ms');
8708											//log('wasOn='+wasOn+' hasLink()='+hasLink($(e.target))+' visible='+ttVisible());
8709											if (hasLink($(e.target)) && ttVisible()) {
8710												// Link and tt is visible :: let click it
8711												return true;
8712											} else {
8713												// No link or need to toggle on first
8714												avoidClick(e);
8715												
8716												if (wasOn) {
8717													hide();
8718												} else {
8719													show();
8720												}
8721												
8722												return false;
8723											}
8724										}		
8725										
8726									} else if (hasLink($(e.target))) {
8727										return true;
8728									}
8729									
8730									if (wasOn) {
8731										clearTimeout(hto);
8732										hide();
8733									}
8734									
8735									//events = '';
8736									overtt = false;
8737								}
8738								
8739								return true;
8740							},
8741						
8742						// Force removing the tooltip
8743						destroy = function(e) {
8744								self.off('.' + ns);
8745								clearTimeout(to);
8746								clearTimeout(hto);
8747								self.data('suppressTooltips', true);
8748								$('#' + self.attr('data-tooltip-id')).remove();
8749								self.attr('data-tooltip-id', null);
8750							};
8751							
8752					if (self.attr('data-tooltip') && typeof self.data('tooltip') === OBJECT) {
8753						options = $.extend({}, options, self.data('tooltip'));
8754						self.removeAttr('data-tooltip');
8755					}
8756					
8757					self.attr('data-tooltip-id', ns)
8758						.data('suppressTooltips', false)
8759						.on('destroyTooltip', destroy)
8760						.on('removeTooltip', hide)
8761						.on('focus.' + ns + ' mouseenter.' + ns /*+ ' ' + TOUCH.START + '.' + ns*/, enter)
8762						.on('blur.' + ns + ' mouseleave.' + ns, leave)
8763						.on('click.' + ns, clicked);
8764					
8765				});
8766		};
8767	
8768	/*	pos:
8769		ALIGN_LEFT = ALIGN_TOP = 0
8770		ALIGN_CENTER = ALIGN_MIDDLE = 1
8771		ALIGN_RIGHT = ALIGN_BOTTOM = 2
8772	*/
8773	$.fn.addTooltip.defaults = {
8774			delay: 				150,
8775			className: 			'tooltip',
8776			nub: 				'nub',
8777			stay: 				2000,
8778			exclusive:			true,
8779			touchToggle:		false,
8780			pos: 				[1,2,1,0],
8781			gap:				6
8782		};
8783	
8784})(jQuery);
8785/*	
8786 *	laza.modal() :: adding modal window to any layer (typically 'body')
8787 *
8788 *	Copyright by Lazaworx
8789 *	http://www.lazaworx.com
8790 *	Author: Laszlo Molnar
8791 *
8792 *	Dual licensed under the MIT and GPL licenses.
8793 *	- http://www.opensource.org/licenses/mit-license.php
8794 *	- http://www.gnu.org/copyleft/gpl.html
8795 *
8796 *	Usage: $(element).modal( content, buttons, options );
8797 */
8798 
8799;(function($) {
8800	'use strict';
8801		
8802	$.fn.modal = function(content, buttons, settings) {
8803			
8804			var $body = $(document.body);
8805
8806			if (typeof content === 'string') {
8807				// Content is a String?
8808				content = $(document.createTextNode(content));
8809			}
8810			
8811			if (!(content instanceof $ && content.length)) {
8812				// Is jQuery element?
8813				return;
8814			}
8815			
8816			if (typeof buttons !== UNDEF && !Array.isArray(buttons)) {
8817				// No buttons? Swap with settings
8818				settings = buttons; 
8819				buttons = null;
8820			}
8821			
8822			if (content.data('modal')) {
8823				// Settings passed as data attribute of the container
8824				settings = $.extend({}, settings, content.data('modal'));
8825			}
8826			
8827			// Merge settings with defaults
8828			settings = $.extend({}, $.fn.modal.defaults, settings);
8829			
8830			var container = $(this),
8831				modals = container.find('.' + settings.modalClass),
8832				lastZ = modals.length? parseInt(modals.last().css('zIndex')) : 300000,
8833				text = getTranslations($.fn.modal.text),
8834				
8835				// Aligning the window to an existing element 
8836				alignPopup = function(modal, to) {
8837					
8838						if (!to.jquery) {
8839							to = $(to);
8840						}
8841						
8842						var toPos = to.position(),
8843							toDim = [ to.outerWidth(), to.outerHeight() ],
8844							wDim = [ modal.outerWidth(), modal.outerHeight() ],
8845							midPos = [ (toPos.left + toDim[0] / 2) / wDim[0], (toPos.top + toDim[1] / 2) / wDim[1] ],
8846							left = null,
8847							right = null,
8848							top = null,
8849							bottom = null,
8850							ctop,
8851							cbottom,
8852							css = {},
8853							ccss = {};
8854
8855						// Horizontal
8856						if (midPos[0] > 0.333 && midPos[0] < 0.667) {
8857							// Center
8858							left = '50%';
8859						} else if (midPos[0] <= 0.333) {
8860							// Left
8861							left = toPos.left + 'px';
8862						} else {
8863							// Right
8864							right = (wDim[0] - (toPos.left + toDim[0])) + 'px';
8865							left = 'auto';
8866						}
8867						
8868						// Vertical
8869						if (midPos[1] <= 0.5) {
8870							// Top
8871							top = (toPos.top + toDim[1] + 4) + 'px';
8872							ctop = (toPos.top + toDim[1] - settings.closeSize) + 'px'; 
8873						} else {
8874							// Bottom
8875							bottom = (wDim[1] - toPos.top + 4) + 'px';
8876							top = 'auto';
8877							cbottom = (wDim[1] - toPos.top - settings.closeSize) + 'px';
8878							ctop = 'auto';
8879						}
8880							
8881						css.left = ccss.left = left;
8882						
8883						if (right !== null) {
8884							css.right = ccss.right = right;
8885						}
8886						
8887						css.top = top;
8888						ccss.top = ctop;
8889						
8890						if (bottom !== null) {
8891							css.bottom = bottom;
8892							ccss.bottom = cbottom;
8893						}
8894						
8895						if (left === '50%') {
8896							css.transform = ccss.transform = 'translateX(-50%)';
8897						} else {
8898							css.transform = ccss.transform = 'none';
8899						}
8900						
8901						// Applying the position
8902						modal.children('.' + settings.windowClass).css(css);
8903						modal.children('.' + settings.closeClass).css(ccss);
8904					},
8905										
8906				// Setting up an existing modal
8907				setupExistingModal = function(content) {
8908						var modal = content.hasClass(settings.modalClass)? content : content.parents('.' + settings.modalClass);
8909						
8910						if (!modal.length || modal.parent()[0] !== container[0]) {
8911							// Moving under container
8912							container.append(modal);
8913						}
8914						
8915						modal.data('keep', true);
8916						
8917						return modal;
8918					},
8919				
8920				createNewModal = function(content, buttons) {
8921						// Creating new modal
8922						var modal,
8923							popup,
8924							closeBtn,
8925							cont;
8926						
8927						$.fn.modal._zIndex = Math.max($.fn.modal._zIndex || 300000, lastZ) + 1;
8928
8929						modal = $('<div>', {
8930								'class': 		settings.modalClass,
8931								zIndex:			$.fn.modal._zIndex
8932							}).appendTo(container);
8933							
8934						modal.data('modal-ns', 'lmo_' + ($.fn.modal._counter = ($.fn.modal._counter || 0) + 1));
8935							
8936						popup = $('<div>', {
8937								'class': 		settings.windowClass + (settings.hasOwnProperty('class')? (' ' + settings['class']) : ''),
8938								role: 			'dialog'
8939							}).appendTo(modal);
8940							
8941						closeBtn = $('<a>', {
8942								'class': 		settings.closeClass,
8943								'title': 		text.closeWindow
8944							}).appendTo((settings.alignTo)? modal : popup);
8945							
8946						closeBtn.addTooltip();
8947						
8948						cont = $('<div>', {
8949								'class': 		settings.contentClass + ' scrollable'
8950							}).appendTo(popup);
8951							
8952						if (settings.title) {
8953							popup.addClass(settings.hasHeaderClass).prepend($('<header>', {
8954									'class':		settings.headerClass + (settings.icon? (' icon-' + settings.icon) : ''),
8955									html: 			settings.title
8956								}));
8957						}
8958						
8959						cont.append(content.show());
8960							
8961						if (typeof buttons !== UNDEF && buttons) {
8962							var i, 
8963								a, 
8964								
8965								btnCont = $('<div>', { 
8966										'class': 		settings.buttonsClass 
8967									}).appendTo(cont),
8968								
8969								clickhandler = function(e) {
8970									e.preventDefault();
8971									var a = e.target;
8972									
8973									if (typeof a.handler === FUNCTION) {
8974										if (a.handler.call(container[0]) !== false) {
8975											close($(a).closest('.' + settings.modalClass));
8976										}
8977									}
8978									return false;
8979								};
8980							
8981							for (i = 0; i < buttons.length; i++) {
8982								a = $('<button>', {
8983										'class': 		[ settings.buttonClass, buttons[i].c ].filter(Boolean).join(' '),
8984										html: 			' ' + buttons[i].t
8985									}).on('click', clickhandler).appendTo(btnCont);
8986								
8987								if (typeof buttons[i].h === FUNCTION) {
8988									a[0].handler = buttons[i].h;
8989								}
8990							}
8991						}
8992						//setMaxHeight();
8993						
8994						return modal;
8995					},
8996					
8997				init = function(content, buttons) {
8998						var modal = document.body.contains(content[0])? 
8999									setupExistingModal(content) 
9000									: 
9001									createNewModal(content, buttons);
9002						
9003						if (settings.hasOwnProperty('closeOnClick')) {
9004							$(settings.closeOnClick).on('click', function() {
9005									close($(this).closest('.' + settings.modalClass));
9006								});
9007						}
9008						
9009						var at = settings.hasOwnProperty('alignTo')? ((settings.alignTo instanceof jQuery)? settings.alignTo : $(settings.alignTo)) : $();
9010						
9011						if (at.length && at.is(':visible')) {
9012							alignPopup(modal, at);
9013						}
9014						
9015						// Prepare
9016						modal.css({
9017							display:		'block',
9018							opacity:		0,
9019							transition:		'opacity ' + settings.speed + 'ms ease-out'
9020						}).removeClass('hidden');
9021						modal[0].offsetHeight;
9022						modal.css({
9023							opacity:		1
9024						})
9025						
9026						$body.addClass(settings.hasModalClass);
9027						
9028						// Externally triggered close
9029						modal.on('destroy', function() {
9030								close($(this));
9031							});
9032						
9033						modal.on('close', function() {
9034								close($(this));
9035							});
9036						
9037						// Close button click handler
9038						modal.find('.' + settings.closeClass).add(modal).on('click.' + modal.data('modal-ns'), function(e) {
9039								if (e.target === this) {
9040									close($(this).closest('.' + settings.modalClass));
9041									if (typeof settings['onClose'] === FUNCTION) {
9042										settings.onClose.call(container[0]);
9043									}
9044									return false;
9045								}
9046							});
9047						
9048						if (settings.autoFade) {
9049							modal.data('to', setTimeout(function() {
9050									close(modal);
9051								}, settings.autoFade));
9052						}
9053
9054						return modal;
9055					},
9056
9057				close = function(modal) {
9058					
9059						if (!modal.length) {
9060							return;
9061						}
9062						
9063						clearTimeout(modal.data('to'));
9064						
9065						modal.destroyAllTooltips();
9066						
9067						modal.one('transitionend', function() {
9068								if (modal.data('keep')) {
9069									modal.css({
9070										display:		'none'
9071									});
9072								} else {
9073									modal.remove();
9074								}
9075								$body.toggleClass(settings.hasModalClass, container.find('.' + settings.modalClass).length > 0);
9076							}).css({
9077								transition:		'opacity ' + settings.speed + 'ms ease-out'
9078							}).css({
9079								opacity:		0
9080							});					
9081					};
9082			
9083			// Starts here
9084			return init(content, buttons);
9085		};
9086	
9087	$.fn.modal.defaults = {
9088			modalClass:			'modal',
9089			windowClass:		'window',
9090			headerClass:		'header',
9091			closeClass:			'close',
9092			contentClass:		'content',
9093			buttonsClass:		'buttons',
9094			buttonClass:		'button',
9095			hasHeaderClass:		'has-header',
9096			hasModalClass:		'has-modal',
9097			speed: 				300,
9098			autoFade: 			0,
9099			width: 				400,
9100			type: 				'normal',
9101			closeSize:			40
9102		};
9103	
9104	$.fn.modal.text = {
9105			closeWindow:					"Close window"
9106		};
9107
9108})(jQuery);
9109/* 
9110 *	Rendering share buttons
9111 */
9112
9113;(function($) {
9114	'use strict';
9115		
9116	$.fn.renderShares = function(settings) {
9117		
9118			settings = $.extend({}, $.fn.renderShares.defaults, settings);
9119			
9120			var self 			= 	$(this),
9121				cont,
9122				previewImage,
9123				shareCaption,
9124				socialLinks,
9125				text 			=	getTranslations($.fn.renderShares.text),
9126				emailSubject	=	settings.emailSubject || text['checkThisOut'],
9127				emailBody		=	settings.emailBody || '{title}\n\r{description}\n\r{link}',
9128				defaultImage,
9129				defaultCaption,
9130				
9131				// Copying URL to the clipboard
9132				
9133				copyUrl = function(e) {
9134						var url = this.querySelector('input[name=url]'),
9135							save,
9136							succ;
9137						
9138						if (url) {
9139							save = url.value;
9140
9141							url.focus();
9142							url.setSelectionRange(0, save.length);
9143							
9144							if (navigator.clipboard && navigator.clipboard.writeText) {
9145								navigator.clipboard.writeText(save).then(function() {
9146									url.value = text['copied'] + '...';
9147									url.classList.add('copied');
9148									setTimeout(function() {
9149										url.value = save;
9150										url.classList.remove('copied');
9151									}, 500);
9152								});
9153							} else {
9154								// Fallback: select for manual copy
9155								url.focus();
9156								url.setSelectionRange(0, save.length);
9157							}
9158						}
9159						
9160						return false;
9161					},
9162					
9163				// Updating URL in the input box
9164				
9165				changeUrl = function(e) {
9166						var opt = this.checked,
9167							option = this.name,
9168							url = this.parentNode.querySelector('input[name=url]');
9169						
9170						if (opt) {
9171							if (url.value.indexOf('#') >= 0) {
9172								if (url.value.search(new RegExp('[#&]' + option)) === -1) {
9173									url.value += '&' + option;
9174								}
9175							} else {
9176								url.value += '#' + option;
9177							}
9178						} else {
9179							url.value = url.value.replace(new RegExp('[\#&]' + option, 'g'), '');
9180						}
9181					},
9182				
9183				// Updating links on trigger (e, opt), or initial
9184				
9185				updateLinks = function(opt) {
9186					
9187						if (typeof opt === UNDEF) {
9188							// Initial
9189							opt = settings;
9190							
9191						} else {
9192							// Triggered
9193
9194							opt = $.extend({}, settings, opt);
9195							
9196							// Reading URL 
9197							if (!opt.hasOwnProperty('href')) {
9198								opt.href = window.location.href;
9199							}
9200							
9201							// Fixing HTML title
9202							opt.title = (opt.title || '').stripHTML();
9203							
9204							// Fixing HTML description
9205							opt.description = (opt.description || '').stripHTML();
9206						}
9207						
9208						// Fixing relative URL
9209						if (opt.image && !opt.image.startsWith('http')) {
9210							opt.image = window.location.href.getDir() + opt.image;
9211						}
9212						
9213						var cc = $(opt.currCardHook);			// Current lightbox image
9214											
9215						// Reading data from current card in the lightbox
9216						if (cc.length) {
9217							
9218							if (!opt.hasOwnProperty('title')) {
9219								// Finding title
9220								opt.title = cc.find('.caption .title').text();
9221								
9222								if (!opt['title']) {
9223									// No title: it's mandatory, so trying to read something from the image / video
9224									var img = cc.find('img, video');
9225									opt.title = img.length?
9226													img.attr('title') || img.attr('alt') || (img.attr('src')? img.attr('src').getFile().replace(/\.(jpg|gif|png|mp4)$/i, '').replace(/_/g, ' ') : '')
9227													:
9228													'';
9229								}
9230							}
9231							
9232							if (!opt.hasOwnProperty('description')) {
9233								// Reading lightbox caption
9234								opt.description = cc.find('.caption .comment').text();
9235							}
9236							
9237							// Image
9238							if (!opt.hasOwnProperty('image')) {
9239								var c = $(opt.currCardHook + ' img');
9240								if (c.length) {
9241									// Image
9242									opt.image = c.attr('src');
9243								} else {
9244									c = $(opt.currCardHook + ' video');
9245									if (c.length) {
9246										// Video
9247										opt.image = c.attr('src').replaceExt('jpg');
9248									} else {
9249										// Nothing found: fall back to meta
9250										c = $('link[rel=image_src]');
9251										opt.image = (c.length)? c.attr('href') : (window.location.href.getDir() + 'folderthumb.jpg');
9252									}
9253								} 
9254							}
9255							
9256							// Updating preview card
9257							if (opt.usePreview) {
9258								
9259								if (opt.hasOwnProperty('image')) {
9260									previewImage.attr('src', opt.image);
9261								}
9262								
9263								shareCaption.empty();
9264								
9265								if (opt.hasOwnProperty('title')) {
9266									shareCaption.append($('<h3>', {
9267										'class':	'title',
9268										text: 		opt.title
9269									}));
9270								}
9271								
9272								if (opt.hasOwnProperty('description')) {
9273									shareCaption.append($('<div>', {
9274										'class':	'description',
9275										text:		opt.description
9276									}));
9277								}
9278							}
9279							
9280						} else {
9281							
9282							// Restore defaults
9283							if (previewImage) previewImage.attr('src', defaultImage);
9284							if (shareCaption) shareCaption.html(defaultCaption);
9285						}
9286						
9287						// Encoded versions
9288						var _href = 		encodeURIComponent(opt.href),
9289							_slideHref =	(opt.href.indexOf('#img=') > 0)? opt.href.replace(settings.indexName, '').replace('#img=', 'slides/').replaceExt(opt.pageExt) : opt.href,
9290							_title = 		encodeURIComponent(opt.title),
9291							_description = 	encodeURIComponent(opt.description),
9292							_image = 		encodeURIComponent(opt.image),
9293							_link;
9294						
9295						self.find('.' + settings.sharesClass + ' a').each(function() {
9296								var a = $(this);
9297								
9298								switch (a.data('share')) {
9299								
9300									case 'facebook':
9301										
9302										if (typeof FB === UNDEF) {
9303											_link = 'https://www.facebook.com/sharer.php?s=100&p%5Burl%5D=' 
9303+ opt.href + '&p%5Bimages%5D%5B0%5D=' + opt.image + '&p%5Btitle%5D=' + _title;
9304										} else {
9305											a.off('.share').on('click.share', function() {
9306												FB.ui({
9307														method: 		'share',
9308														href:			_slideHref,
9309														caption: 		opt.title,
9310														description: 	opt.description,
9311														picture: 		opt.image
9312													});
9313												return false;
9314											});
9315											return;
9316										}
9317										break;
9318										
9319									case 'threads':
9320										_link = 'https://threads.net/intent/post?text=' + _title + ':%20' + _slideHref;
9321										break;
9322										
9323									case 'bluesky':
9324										_link = 'https://bsky.app/intent/compose?text=' + _title + ':%20' + _slideHref;
9325										break;
9326									
9327									case 'twitter':			
9328										_link = 'https://twitter.com/intent/tweet?text=' + _title + ':%20' + _href;
9329										break;
9330										
9331									case 'tumblr':
9332										_link = 'https://www.tumblr.com/share/link?url=' + _href + '&name=' + _title; 
9333										break;
9334										
9335									case 'pinterest':
9336										_link = 'https://pinterest.com/pin/create/button/?url=' + _href + '&media=' + _image + '&description=' + _title;
9337										break;
9338									
9339									case 'linkedin':
9340										_link = 'https://www.linkedin.com/shareArticle?mini=true&url=' + _href + '&title=' + _title + '&summary=' + _description;
9341										break;
9342										
9343									case 'digg':
9344										_link = 'https://digg.com/submit?url=' + opt.href;
9345										break;
9346										
9347									case 'reddit':
9348										_link = 'https://www.reddit.com/submit?url=' + opt.href;
9349										break;
9350										
9351									case 'email':
9352										_link = 'mailto:?subject=' + encodeURIComponent(emailSubject.replace('{title}', opt.title).replace('{description}', opt.description).replace('{album}', settings.albumTitle || '')) + 
9353													'&body=' + encodeURIComponent(emailBody.replace('{title}', opt.title).replace('{description}', opt.description).replace('{link}', opt.href).replace('{album}', settings.albumTitle || ''));
9354										break;
9355										
9356									case 'link':
9357										var tt = $('<form class="copy-url">' +
9358												'<input name="url" type="text" readonly value="' + opt.href + '">' +
9359												'<input name="slideshow" id="slideshowcb" type="checkbox">' +
9360												'<label for="slideshowcb">' + text['slideshow'] + '</label>' +
9361												' <button type="submit" class="button icon-copy" data-closer> ' + text['copy'] + '</button>' +
9362											'</form>');
9363										
9364										tt.on('submit', copyUrl);
9365										tt.find('[name=slideshow]').on('change', changeUrl);
9366										
9367										a.addClass('copy-url-btn')
9368											.trigger('destroyTooltip').addTooltip(tt, {
9369												stay:			4000,
9370												pos: 			settings.pos,
9371												touchToggle:	true
9372											});
9373										
9374										return;
9375										
9376									default:
9377										return;
9378										
9379								}
9380							
9381								a.attr({
9382										href: 		_link,
9383										target: 	'_blank'
9384									});
9385								
9386							});
9387					},
9388			
9389				// Creating like buttons initially 
9390				
9391				createButtons = function(target) {
9392						
9393						var buttons = 		settings.buttons.split(','),
9394							_href = 		encodeURIComponent(window.location.href.split('#')[0]),
9395							_title = 		encodeURIComponent(settings.title || $('meta[name=title]').attr('content') || $('title').text()),
9396							_image = 		settings.image? 
9397												((settings.image.startsWith('http')? '' : window.location.href.getDir()) + 
9398													encodeURIComponent(settings.image)) 
9399												: 
9400												$('link[rel=image_src]').attr('href'),
9401							btns = 			$('<div>', {
9402													'class': 	settings.buttonsClass
9403												}).appendTo(target || self);
9404						
9405						for (var i = 0; i < buttons.length; i++) {
9406							
9407							switch (buttons[i]) {
9408								
9409								case 'facebook':
9410									btns.append('<div class="likebtn fb-like" data-href="' + _href + '" data-layout="button_count" data-action="like" data-size="large" data-show-faces="false" data-share="false"></div>');
9411									break;
9412								
9413								case 'twitter':
9414									btns.append('<a class="likebtn twitter-share-button icon-x" href="https://twitter.com/intent/tweet?text=' + _title + '&url=' + _href + '" data-size="large" target="_blank"> Tweet</a>');
9415									break;
9416	
9417								case 'pinterest':
9418									btns.append('<div class="likebtn"><a data-pin-count="beside" data-pin-tall="true" data-pin-save="true" href="https://www.pinterest.com/pin/create/button/?url=' + _href + '&media=' + _image + '&description=' + _title + '" data-pin-do="buttonPin"></a></div>');
9419									break;
9420							}
9421						}
9422					},
9423				
9424				// Creating share links the first time
9425				
9426				createLinks = function(target) {
9427						var n,
9428							links = $('<div>', {
9429									'class': 	settings.sharesClass
9430								}).appendTo(target || self),
9431							sites = settings.sites.split(','),
9432							validSites = ['facebook','threads','bluesky','twitter','tumblr','pinterest','linkedin','digg','reddit','email','link'];
9433						
9434						for (var i = 0, a; i < sites.length; i++) {
9435							
9436							n = sites[i];
9437							
9438							if (validSites.indexOf(n.trim()) >= 0) {
9439								a = $('<a>', {
9440										'class': 	settings.btnClass + ' icon-' + ((n === 'twitter')? 'x' : n),
9441										rel:		'noopener',
9442										text:		' ' + (settings.buttonLabels? n.capitalize() : '')
9443									})
9444									.data('share', n)
9445									.appendTo(links);
9446								
9447								if (n === 'email') {
9448									a.data('tooltip', text['email']);
9449								} else if (n !== 'link') {
9450									a.data('tooltip', text['shareOn'] + ' ' + ((n === 'twitter')? 'X' : n.capitalize()));
9451								}
9452							}
9453						}
9454						
9455						updateLinks();
9456						
9457						links.children('a').not('.copy-url-btn').addTooltip({ 
9458								pos: 	settings.pos 
9459							});
9460					},
9461					
9462				// Trying to find a title
9463				getDefaultTitle = function() {
9464						var t = self.find('h3,.title');
9465						// self-contained
9466						if (t.length && (t = t.text())) {
9467							return t;
9468						}
9469						// Opengraph title
9470						t = $('meta[property="og:title"]');
9471						if (t.length && (t = t.attr('content'))) {
9472							return t;
9473						}
9474						// <title> tag
9475						return $('title').text() || '';
9476					},
9477					
9478				// Trying to find a description
9479				getDefaultDescription = function() {
9480						var t = self.find('.description,.comment');
9481						// self-contained
9482						if (t.length && (t = t.text())) {
9483							return t;
9484						}
9485						// Opengraph description
9486						t = $('meta[property="og:description"]');
9487						if (t.length && (t = t.attr('content'))) {
9488							return t;
9489						}
9490						// <meta> description tag
9491						t = $('meta[name=description]');
9492						if (t.length && (t = t.attr('content'))) {
9493							return t;
9494						}
9495						return '';
9496					};
9497			
9498			// Reading smart defaults if missing
9499			
9500			// URL
9501			if (!settings.hasOwnProperty('href')) {
9502				settings.href = window.location.href;
9503			}
9504			
9505			// Image
9506			if (!settings.hasOwnProperty('image')) {
9507				var i = $('link[rel=image_src]');
9508				settings.image = (i.length)? i.attr('href') : (window.location.href.getDir() + 'folderthumb.jpg');
9509			}
9510			
9511			// Title
9512			if (!settings.hasOwnProperty('title')) {
9513				settings.title = getDefaultTitle();
9514			}
9515			
9516			// Description
9517			if (!settings.hasOwnProperty('description')) {
9518				settings.description = getDefaultDescription();
9519			}
9520			
9521			// Page extension
9522			if (!settings.hasOwnProperty('pageExt')) {
9523				settings.pageExt = settings.indexName.getExt();
9524			}
9525			
9526			if (settings.usePreview) {
9527				// Complete share card with preview
9528				
9529				// Wrapping element
9530				cont = self.find('.' + settings.contClass);
9531				
9532				if (!cont.length) {
9533					cont = $('<div>', {
9534							'class':	settings.contClass
9535						}).appendTo(self);
9536				}
9537				
9538				// Image
9539				previewImage = cont.find('img');
9540				
9541				if (!previewImage.length) {
9542					if (settings['image']) {
9543						previewImage = $('<img>', {
9544								src:		settings.image				// Relative URL is fine
9545							}).appendTo(cont);
9546					} else {
9547						cont.addClass('no-image');
9548					}
9549				}
9550				
9551				// Caption
9552				shareCaption = cont.find('.' + settings.captionClass);
9553				
9554				if (!shareCaption.length) {
9555					shareCaption = $('<div>', {
9556							'class':	settings.captionClass,
9557							html:		'<h3 class="title">' + settings.title + '</h3>' + 
9558										(settings.description? ('<div class="description">' + settings.description + '</div>') : '')
9559						}).appendTo(cont);
9560				}
9561		
9562				defaultCaption = shareCaption.html();
9563		
9564				// Social links wrap
9565				socialLinks = cont.find('.' + settings.socialLinksClass);
9566				
9567				if (!socialLinks.length) {
9568					socialLinks = $('<div>', {
9569							'class':	settings.socialLinksClass
9570						}).appendTo(cont);
9571				}
9572				
9573			} else {
9574				// Simple share buttons, no card
9575				socialLinks = self;
9576			}
9577			
9578			// Creating simple links
9579			createLinks(socialLinks);
9580			
9581			// Creating button
9582			if (settings.hasOwnProperty('buttons') && !LOCAL) {
9583				createButtons(socialLinks);
9584			}
9585			
9586			if (self.closest('.modal').length) {
9587				// On a modal window
9588				self.find('.' + settings.sharesClass + ' a').not('.copy-url-btn').on('click', function() {
9589						// Close modal on clicking a button
9590						$(this).closest('.modal').trigger('close');
9591						return true;
9592					});
9593			}
9594			
9595			// External triggering
9596			self.on('updateLinks', function(e, opt) {
9597					updateLinks(opt);
9598				});
9599					
9600			return this;
9601		};
9602	
9603	$.fn.renderShares.defaults = {
9604			buttonLabels:			false,
9605			usePreview:				true,
9606			contClass:				'preview',
9607			captionClass:			'caption',
9608			socialLinksClass:		'social-links',
9609			sharesClass: 			'shares',
9610			buttonsClass:			'buttons',
9611			btnClass:				'btn',
9612			sites: 					'facebook,twitter,tumblr',
9613			indexName:				'index.html',
9614			currCardHook:			'.lightbox .curr',
9615			emailSubject:			'',
9616			emailBody:				'',
9617			pos:					[2,1,0,1]
9618		};
9619
9620	$.fn.renderShares.text = {
9621			checkThisOut:					"Found this page",
9622			copied:							"Copied",
9623			slideshow:						"Slideshow",
9624			copy:							"Copy",
9625			email:							"Email",
9626			shareOn:						"Share on"
9627		};
9628
9629})(jQuery);
9630
9631/* Generated structure
9632<div class="share-cont social-links text-center">
9633	<div class="thumb">
9634		<img src="http://path.to/image">
9635		<div class="caption">
9636			<h3>Title</h3>
9637			<div class="description">Description</div>
9638		</div>
9639		<div class="social-links">
9640			<div class="shares">
9641				<a class="btn icon-facebook" rel="noopener" data-tooltip-id="_ltt_1742"> </a>
9642				<a class="btn icon-twitter" rel="noopener" href="https://twitter.com/home?status=Patrik: http%3A%2F%2F127.0.0.1%3A8080%2FTiger%2FPeople%2Findex.html%23img%3DPatrik.jpg" target="_blank" data-tooltip-id="_ltt_4248"> </a>
9643				<a class="btn icon-email" rel="noopener" href="mailto:?subject=Check%20this%20out&amp;body=Patrik%0D%0APeople%0D%0Ahttp://127.0.0.1:8080/Tiger/People/index.html#img=Patrik.jpg" target="_blank" data-tooltip-id="_ltt_6930"> </a>
9644			</div>
9645		</div>
9646	</div>
9647</div>
9648*/
9649;(function($, $window) {
9650	'use strict';
9651				
9652	/* 
9653		sticky.js - renders 'sticky' class to off-screen layers
9654	*/
9655
9656	$.fn.sticky = function(options) {
9657		
9658			options = $.extend({
9659							stickyClass:			'sticky',					// Sticky class
9660							stickToEdge:			'st-stick',					// The first/last element that need to be stick to edge class
9661							topmostClass:			'st-topmost',				// First element but not stick to window top
9662							bottommostClass:		'st-bottommost',			// First element but not stick to window top
9663							topClass:				'st-top',					// Top class
9664							bottomClass:			'st-bottom',				// Bottom class
9665							placeholderClass:		'st-placeholder',			// Placeholder class
9666							controlsClass:			'st-controls',				// Minimize/Maximize controls
9667							minimizedClass:			'minimized',
9668							maximizedClass:			'maximized',
9669							watch:					'.thumbnails',				// Only if this section is on screen
9670							watchThreshold:			100,						// The height to be on screen to be considered as visible
9671							minimizableThreshold:	80,							// Minimized controls added only above this height
9672							placeBelow:				'.top-bar.sticky'			// Extra top position to add above
9673						}, options);
9674			
9675			var ns = 'lst_' + ($.fn.sticky._counter = ($.fn.sticky._counter || 0) + 1),
9676				st = [],
9677				last = -999,
9678				
9679				// Make panel static
9680				
9681				makeStatic = function(i) {
9682						var el = $('[data-sticky-rel=' + st[i].id + ']'),
9683							p = $('#' + st[i].id);
9684							
9685						if (el.length && el.hasClass(options.stickyClass)) {
9686							// Hiding placeholder
9687							p.css('height', '');
9688							// On screen
9689							el.removeClass(options.stickyClass + ' ' + options.topClass + ' ' + options.bottomClass);
9690						}
9691					},
9692					
9693				// Make all panels static
9694				
9695				makeAllStatic = function() {
9696					
9697						for (var i = 0; i < st.length; i++) {
9698							makeStatic(i);
9699						}
9700					},
9701					
9702				// Recalculate panel positions after minimize / maximize
9703				
9704				recalcPos = function(bottom) {
9705						var el = bottom? 
9706							$('.' + options.bottomClass + '.' + options.stickyClass + ':visible').backwards()
9707							: 
9708							$('.' + options.topClass + '.' + options.stickyClass + ':visible'),
9709						offs = bottom? 0 : $(options.placeBelow + ':visible').outerHeight() || 0;
9710						
9711						el.each(function() {
9712								$(this).css(bottom? 'bottom':'top', offs);
9713								offs += $(this).outerHeight();
9714							});
9715					},
9716					
9717				// Checking all elements
9718				
9719				checkPos = function() {
9720						var wt = $window.scrollTop(),
9721							wh = $window.outerHeight(),
9722							watch = $(options.watch + ':visible'),
9723							offsTop,
9724							offsBtm,
9725							topmost,
9726							bottommost,
9727							wpt;
9728						
9729						if (Math.abs(wt - last) <= 2) {
9730							// Already checked
9731							return;
9732						}
9733						
9734						last = wt;
9735						
9736						if (watch.length) {
9737							wpt = watch.position().top;
9738						}
9739						
9740						if (!watch.length || (wpt > wt + wh - options.watchThreshold) || (wpt + watch.outerHeight() <= wt + options.watchThreshold)) {
9741							// No watch element or not on screen
9742							
9743							makeAllStatic();
9744							
9745						} else {
9746							// Watch element is on screen
9747							
9748							offsTop = $(options.placeBelow + ':visible').outerHeight() || 0;
9749							topmost = true;
9750							
9751							// Calculating top position
9752							$('.' + options.topClass + '.' + options.stickyClass + ':visible').each(function() {
9753									offsTop += $(this).outerHeight();
9754									topmost = false;
9755								});
9756							
9757							offsBtm = 0;
9758							bottommost = true;
9759							
9760							// Calculating bottom position
9761							$('.' + options.bottomClass + '.' + options.stickyClass + ':visible').each(function() {
9762									offsBtm += $(this).outerHeight();
9763									bottommost = false;
9764								});
9765							
9766							for (var i = 0, el, p, pt; i < st.length; i++) { 
9767								el = $('[data-sticky-rel=' + st[i].id + ']');
9768								p = $('#' + st[i].id);
9769								
9770								if (el.length && p.length) {
9771									// Panel's top position
9772									pt = p.position().top;
9773								
9774									if (pt < (wt + offsTop) || (pt + st[i].height) > (wt + wh - offsBtm)) {
9775										// Place as sticky
9776										if (!el.hasClass(options.stickyClass)) {
9777											// Make the placeholder same height
9778											p.height(st[i].height);
9779											el.addClass(options.stickyClass);
9780											
9781											if (pt < (wt + offsTop)) {
9782												// Above
9783												//pt = getPos(options.topClass);
9784												
9785												if (offsTop) {
9786													// Offsetting already existing stickies
9787													el.removeClass(options.stickToEdge).css('top', offsTop);
9788												} else {
9789													el.addClass(options.stickToEdge);
9790												}
9791												
9792												offsTop += st[i].height;
9793												
9794												if (topmost) {
9795													el.addClass(options.topmostClass);
9796													topmost = false;
9797												}
9798												
9799												el.addClass(options.topClass).removeClass(options.bottomClass);
9800												
9801											} else {
9802												// Below
9803												el.addClass(options.bottomClass).removeClass(options.topClass);
9804											}
9805										}
9806										
9807									} else {
9808										// Place as static
9809										makeStatic(i);
9810									}
9811								}
9812							}
9813							
9814							offsBtm = 0;
9815							
9816							$('.' + options.bottomClass + '.' + options.stickyClass).backwards().each(function() {
9817									$(this).css('bottom', offsBtm);
9818									
9819									if (offsBtm) {
9820										$(this).removeClass(options.stickToEdge);
9821									} else {
9822										$(this).addClass(options.stickToEdge);
9823									}
9824									
9825									offsBtm += $(this).outerHeight();
9826									
9827									if (bottommost) {
9828										$(this).addClass(options.bottommostClass);
9829										bottommost = false;
9830									}
9831								});
9832						}
9833						
9834						return true;
9835					};
9836					
9837			// Preprocessing elements
9838			this.each(function(i) {
9839					var el = $(this),
9840						cont = el.children().first(),
9841						id = ns + '_' + i,
9842						h = el.outerHeight();
9843					
9844					el.before($('<div>', {
9845							id:				id,
9846							'class': 		options.placeholderClass
9847						}));
9848					
9849					if (h > options.minimizableThreshold) {
9850						cont.prepend($('<div>', {
9851								'class':    options.controlsClass
9852							}).append($('<a>', {
9853									'class':    'minimize'
9854									}).on('click', (function(i) {
9855										return function() {
9856											el.removeClass(options.maximizedClass).addClass(options.minimizedClass);
9857											st[i].height = el.outerHeight();
9858											recalcPos(el.hasClass(options.bottomClass));
9859										};
9860									}(i)))).append($('<a>', {
9861										'class':    'maximize'
9862									}).on('click', function() {
9863										el.removeClass(options.minimizedClass).addClass(options.maximizedClass);
9864										recalcPos(el.hasClass(options.bottomClass));
9865									}))
9866							);
9867					}
9868					
9869					el.attr('data-sticky-rel', id).addClass(options.maximizedClass);
9870					
9871					st.push({
9872							id:				id,
9873							height:			h
9874						});
9875				});
9876			
9877			var els = $(this);
9878			
9879			els.on('stickyRefresh', function() {
9880					// Re-measure heights
9881					els.each(function(i) {
9882						st[i].height = $(this).outerHeight();
9883					});
9884					last = -999;
9885					checkPos();
9886				});
9887			
9888			// Scroll event listener
9889			$window.on('scroll.' + ns + ' resize.' + ns, function() {
9890					checkPos();
9891				});
9892			
9893			// Initial check
9894			checkPos();
9895			
9896			return this;
9897		};
9898	
9899}(jQuery, jQuery(window)));
9900/*	
9901 *	swipe() :: Swipe action using CSS transforms with jQuery position fallback
9902 *
9903 *	Copyright by Lazaworx
9904 *	http://www.lazaworx.com
9905 *	Author: Laszlo Molnar
9906 *
9907 *	Dual licensed under the MIT and GPL licenses.
9908 *	- http://www.opensource.org/licenses/mit-license.php
9909 *	- http://www.gnu.org/copyleft/gpl.html
9910 *
9911 *	Usage: $(element).swipe( options );
9912 */
9913 
9914;(function($) {
9915	'use strict';
9916	
9917	$.fn.swipe = function(options) {
9918		
9919		options = $.extend({
9920						onSwipedLeft:				false,			// Swiped left event
9921						onSwipedRight:				false,			// Swiped right event
9922						onSwipedUp:					false,			// Swiped up event
9923						onSwipedDown:				false,			// Swiped down event
9924						onSmallSwipe:				false,			// Small swipe (< threshold px)
9925						onSwipeStart:				false,			// Start swiping event
9926						onPinchZoom:				false,			// Pinch zoomed event
9927						onClick:					false,			// Clicked
9928						onDoubleClick:				false,			// Doble-clicked
9929						onFinished:					false,			// Swiped out - can remove
9930						threshold:					20,				// Absolute floor (px) for the swipe distance
9931						tapTolerance:				10,				// Max finger travel (px) still counted as a tap (click / double-tap)
9932						minSwipeFraction:			0.06,			// Min swipe travel to navigate, as fraction of stage width (avoids accidental navigation); 0 disables
9933						clickThreshold:				150,			// Timespan to tell apart single and double click
9934						doubleClickThreshold:		350,			// Two clicks come in shorter time treated as double-click 
9935						noSwipeInside:				'.caption',		// Avoid swipe inside this container
9936						stopAutoPano:				true,			// Stops autopano move, and doesn't trigger click if done so
9937						swipeoutDuration:			300,			// Swipe out speed in ms
9938						avoidNavigationOnZoomed:	true,			// Allow navigation on large images 
9939						overThreshold:				0.2,			// Oversized pictures must moved beyond (this * screen width) to trigger swipe 
9940						minZoomFraction:			0.4,			// Min zoom: image may shrink to this fraction of the stage (fit dimension)
9941						maxZoomNatural:				2				// Max zoom: this * the image's natural pixel size (falls back to * fit size if natural size is unknown)
9942					}, options);
9943		
9944		// Getting coordinates of a touch or mouse event
9945		var	getDim = function(el) {
9946					return [ el.outerWidth(), el.outerHeight() ];
9947				},
9948			
9949			getPos = function(e) {
9950					if (e.touches) {
9951						if (e.touches.length > 1) {
9952							// Two finger event
9953							return [ e.touches[0].clientX, e.touches[0].clientY, e.touches[1].clientX, e.touches[1].clientY ];
9954						}
9955						// Single finger event
9956						return [ e.touches[0].clientX, e.touches[0].clientY ];
9957					}
9958					// mouse event
9959					return (e.clientX === null)? null : [ e.clientX, e.clientY ];
9960				};
9961		
9962		return this.each(function() {
9963				
9964				var self = $(this),							// target element
9965					cont = self.parent(),					// container or clipping window
9966					ns,										// namespace 
9967					rto,									// window resize update timeout
9968					// Dimensions
9969					selfDim,								// item dimensions
9970					clipDim,								// clip dimensions
9971					scale,									// scaling
9972					minScale,								// min total scale allowed this gesture
9973					maxScale,								// max total scale allowed this gesture
9974					panRect,								// image edges relative to container at gesture start
9975					// Event timestamps
9976					startTime,								// start time
9977					lastTime,								// previous event time (to calculate speed)
9978					endTime,								// end time
9979					// Event coordinates (screen-relative)
9980					startPos,								// initial position Array[2..4]
9981					currPos,								// Current position
9982					imgStartCenter,                         // Visual center of image at swipe start
9983					// Etc
9984					speed,									// move speed [ vx, vy ] in px/ms
9985					startD,									// initial distance of two finger events
9986					currD,
9987					mx,										// original matrix
9988					// Flags
9989					hasStarted = false,						// flag to avoid double start on touch devices
9990					hasEnded = true,						// ended swiping
9991					swipeHandled = true,					// swipe event already fired
9992					allowNavigation = false,				// Allow navigation (true if small, pano or settings.avoidNavigationOnZoomed)
9993					dontSwipeUp = true,						// limit vertical swipe
9994					dontSwipeDown = true,
9995					canConstrainHorz = false,				// Can constrain to horizontal
9996					nonSwipeable = true,					// target element swipeable?
9997					clickedOnce = false,					// Check for double click
9998					hadAutoPano = false,					// Remembering if autopano was on
9999					chkDoubleClick = null,					// Checking double-click (timeout)
10000					
10001					// Handling swipe start event
10002					handleSwipeStart = function(e) {
10003							if (typeof options.onSwipeStart === FUNCTION) {
10004								options.onSwipeStart.call(self[0], e);
10005							}
10006						},
10007					
10008					// Handling swipeout movement finished event
10009					handleFinished = function(e) {
10010							//log('handleFinished');
10011							self.data('scrolling', false);
10012							if (typeof options.onFinished === FUNCTION) {
10013								options.onFinished.call(self[0], e);
10014							}
10015						},
10016					
10017					// Handling small swipe
10018					handleSmallSwipe = function(e) {
10019							//log('handleSmallSwipe');
10020							unregisterEvents(e);
10021							self.data('scrolling', false);
10022							if (typeof options.onSmallSwipe === FUNCTION) {
10023								options.onSmallSwipe.call(self[0], e);
10024							}
10025						},
10026					
10027					// Handling click event
10028					handleClick = function(e) {
10029							//log('handleClick');
10030							unregisterEvents(e);
10031							self.data('scrolling', false);
10032							if (!hadAutoPano && typeof options.onClick === FUNCTION) {
10033								// Avoid click when stopping panomove consumed the event
10034								options.onClick.call(self[0], e);
10035							}
10036						},
10037					
10038					// Handling doubleclick event
10039					handleDoubleClick = function(e) {
10040							//log('handleDoubleClick');
10041							unregisterEvents(e);
10042							self.data('scrolling', false);
10043							if (typeof options.onDoubleClick === FUNCTION) {
10044								options.onDoubleClick.call(self[0], e);
10045							}
10046						},
10047					
10048					// Handling swipe		
10049					handleSwipedDown = function(e) {
10050							//log('handleSwipe:'+(right?'right':'left'));
10051							if (!swipeHandled && typeof options.onSwipedDown === FUNCTION) {
10052								unregisterEvents(e);
10053								swipeHandled = true;
10054								// Swiped down
10055								options.onSwipedDown.call(self[0], e);
10056							}
10057						},
10058						
10059					// Handling swipe		
10060					handleSwipedUp = function(e) {
10061							//log('handleSwipe:'+(right?'right':'left'));
10062							if (!swipeHandled && typeof options.onSwipedUp === FUNCTION) {
10063								unregisterEvents(e);
10064								swipeHandled = true;
10065								// Swiped up
10066								options.onSwipedUp.call(self[0], e);
10067							}
10068						},
10069						
10070					// Handling swipe		
10071					handleSwipedRight = function(e) {
10072							//log('handleSwipe:'+(right?'right':'left'));
10073							if (!swipeHandled && typeof options.onSwipedRight === FUNCTION) {
10074								unregisterEvents(e);
10075								swipeHandled = true;
10076								// Swiped right
10077								options.onSwipedRight.call(self[0], e);
10078							}
10079						},
10080						
10081					// Handling swipe		
10082					handleSwipedLeft = function(e) {
10083							//log('handleSwipe:'+(right?'right':'left'));
10084							if (!swipeHandled && typeof options.onSwipedLeft === FUNCTION) {
10085								unregisterEvents(e);
10086								swipeHandled = true;
10087								// Swiped left
10088								options.onSwipedLeft.call(self[0], e);
10089							}
10090						},
10091					
10092					// Handling pinch zoom
10093					handlePinchZoom = function(e) {
10094							//log('handlePinchZoom:'+(right?'right':'left'));
10095							if (!swipeHandled && typeof options.onPinchZoom === FUNCTION) {
10096								unregisterEvents(e);
10097								swipeHandled = true;
10098								// Swiped left
10099								options.onPinchZoom.call(self[0], e);
10100							}
10101						},
10102						
10103					// Handling right click event
10104					handleRightClick = function(e) {
10105							//log('handleRightClick');
10106							unregisterEvents(e);
10107							if (typeof options.onRightClick === FUNCTION) {
10108								options.onRightClick.call(self[0], e);
10109							}
10110						},
10111					
10112					// Registering events
10113					registerEvents = function(e) { 
10114							//log('registerEvents');
10115							if (e.type === 'mousedown') {
10116								self.on('mouseup.' + ns + ' mouseout.' + ns, swipeEnd);
10117								$(document).on('mousemove.' + ns, swipeMove);
10118							} else if (e.type === TOUCH.START) {
10119								self.on(TOUCH.END + '.' + ns, swipeEnd);
10120							}
10121						},
10122					
10123					unregisterEvents = function(e) {
10124							//log('unregisterEvents');
10125							if (e.type === 'mouseup' || e.type === 'mouseout') {
10126								self.off('mouseup.' + ns + ' mouseout.' + ns);
10127								$(document).off('mousemove.' + ns);
10128							} else if (e.type === TOUCH.END) {
10129								self.off(TOUCH.END + '.' + ns);
10130							}
10131						},
10132					
10133					// Getting distance of two finger events
10134					getDistance = function(p) {
10135							return Math.hypot(p[2] - p[0], p[3] - p[1]);
10136						},
10137					
10138					// Move to
10139					// p = event coordinates (2 = one finger, 4 = 2 finger gesture)
10140					// op = opacity (null = don't care
10141					// duration = transition speed (ms)
10142					// doneFn = callback
10143					transformTo = function(p, op, duration, doneFn) {
10144							var css = {},
10145								dx,
10146								dy;
10147							
10148							if (p.length === 4) {
10149								// --- 2 Finger Move (Pinch Zoom) ---
10150								
10151								// 1. Scale relative to the start of this gesture
10152								scale = getDistance(p) / startD;
10153					
10154								// 1b. Clamp the *total* zoom to the configured min / max
10155								var totalScale = mx[0] * scale;
10156								if (totalScale < minScale) {
10157									totalScale = minScale;
10158								} else if (maxScale && totalScale > maxScale) {
10159									totalScale = maxScale;
10160								}
10161								scale = totalScale / mx[0];
10162					
10163								// 2. Get Finger Center
10164								var currCX = (p[0] + p[2]) / 2,
10165									currCY = (p[1] + p[3]) / 2,
10166									startCX = (startPos[0] + startPos[2]) / 2,
10167									startCY = (startPos[1] + startPos[3]) / 2;
10168					
10169								// 3. Calculate Pan (Finger movement)
10170								dx = currCX - startCX;
10171								dy = currCY - startCY;
10172					
10173								// 4. Focal Point Fix
10174								// We use the cached imgStartCenter calculated in swipeStart.
10175								// This ensures we pull the image based on where it ACTUALLY was when you started pinching.
10176								dx += (startCX - imgStartCenter[0]) * (1 - scale);
10177								dy += (startCY - imgStartCenter[1]) * (1 - scale);
10178					
10179							} else {
10180								// --- 1 Finger Move ---
10181								dx = p[0] - startPos[0];
10182								dy = p[1] - startPos[1];
10183							}
10184							
10185							css.transform = 'matrix(' + (mx[0] * scale) + ',' + mx[1] + ',' + mx[2] + ',' + (mx[3] * scale) + ',' + (mx[4] + dx) + ',' + (mx[5] + dy) + ')';
10186							
10187							if (typeof duration !== UNDEF && duration) {
10188								css.transition = 'transform ' + duration + 'ms ease-in-out';
10189								if (op !== null) {
10190									css.transition += ',opacity ' + duration + 'ms ease-in-out';
10191									css.opacity = op;
10192								}
10193							} else {
10194								css.transition = 'none';
10195							}
10196							
10197							if (typeof doneFn === FUNCTION) {
10198								self.one('transitionend', doneFn);
10199							}
10200							
10201							self.css(css);
10202						},
10203					
10204					// Starting swipe
10205					swipeStart = function(e) {
10206							//log('swipeStart:' + e.type);
10207							
10208							//log('Touch start, fingers: ' + (e.originalEvent.touches? e.originalEvent.touches.length : 0));
10209							if (pageZoomed() ||
10210								e.originalEvent.touches && e.originalEvent.touches.length > 2 ||
10211								(options.noSwipeInside && $(e.target).closest(options.noSwipeInside).length) ||
10212								e.target.nodeName === 'A' ||
10213								e.target.nodeName === 'INPUT' ||
10214								e.target.nodeName === 'BUTTON' ||
10215								e.target.nodeName === 'AUDIO' || 
10216								(e.target.nodeName === 'VIDEO' && 
10217									(!e.target.paused ||
10218										e.target.controls && 
10219										(ISIOSDEVICE || e.offsetY > ($(e.target).height() - 60))
10220									)
10221								)
10222							) {
10223								// Don't swipe buttons and links and other non-swipeable elements
10224								// No swiping on iOS video player
10225								nonSwipeable = true;
10226								return true;
10227							}
10228							
10229							if (e.type === 'mousedown' && e.which !== 1) {									
10230								// Right click
10231								if (options.stopAutoPano && cont.data('panomove')) {
10232									self.trigger('autopanoStop');
10233								}
10234								handleRightClick(e);
10235								return true;
10236							}
10237							
10238							if (hasStarted) {
10239								// No mouseup / touchend registered yet
10240								return false;
10241							}
10242							
10243							if (e.type !== 'touchstart') {
10244								// Cancel bubbling, but not break long tap
10245								e.preventDefault();
10246							}
10247							
10248							hasStarted = true;
10249							nonSwipeable = false;
10250							hasEnded = false;
10251							// Cancel any pending single-click timer from a previous tap so it
10252							// can't fire mid-gesture, but KEEP clickedOnce so a quick second
10253							// tap is still recognised as a double-tap.
10254							if (chkDoubleClick) {
10255								clearTimeout(chkDoubleClick);
10256								chkDoubleClick = null;
10257							}
10258							
10259							if (options.stopAutoPano && cont.data('panomove')) {
10260								self.trigger('autopanoStop');
10261								hadAutoPano = true;
10262							} else {
10263								hadAutoPano = false;
10264							}
10265							
10266							self.data({
10267									scrolling: 		true,
10268									swipeEnded:		false,
10269									taplength:		0
10270								});
10271							
10272							startTime = lastTime = new Date().getTime();
10273							startPos = getPos(e.originalEvent);
10274							currPos  = startPos.slice();
10275							speed = [ 0, 0 ];
10276							if (startPos.length === 4) {
10277								startD = getDistance(startPos);
10278							}
10279							clipDim = getDim(cont);
10280							selfDim = getDim(self);
10281							mx = self.getMatrix();
10282							scale = 1;
10283							
10284							// Zoom limits for this gesture (absolute total scale, relative to layout size).
10285							// minScale: image may shrink to minZoomFraction of the stage in its fit dimension.
10286							// maxScale: maxZoomNatural * (natural px / layout px) so it caps at e.g. 300% of
10287							//           the real pixels; if natural size is unknown, cap at maxZoomNatural * fit.
10288							var fitRatio = Math.min(clipDim[0] / selfDim[0], clipDim[1] / selfDim[1]),
10289								media = (self[0].naturalWidth || self[0].videoWidth) ? self[0]
10290											: (self.find('img,video')[0] || self[0]),
10291								natW = media.naturalWidth || media.videoWidth || 0;
10292							minScale = Math.min(1, options.minZoomFraction * fitRatio);
10293							maxScale = Math.max(1, natW ? options.maxZoomNatural * natW / selfDim[0]
10294														: options.maxZoomNatural);
10295							
10296							// Image's actual visual center, straight from its rendered box.
10297							// Includes the current transform, so it's correct at any zoom level
10298							// and independent of transform-origin and the left/top/translate trick.
10299							var srect = self[0].getBoundingClientRect();
10300							imgStartCenter = [
10301								srect.left + srect.width  / 2,
10302								srect.top  + srect.height / 2
10303							];
10304							
10305							// Image edges relative to the container, for the boundary / over-swipe
10306							// math. Measured from the live rendered boxes, so it's correct for any
10307							// transform-origin and any CSS centering trick (no scale/translate model
10308							// assumptions). Translation maps 1:1 to px, so a pan of d shifts every
10309							// edge by d.
10310							var crect = cont[0].getBoundingClientRect();
10311							panRect = {
10312								l: srect.left   - crect.left,
10313								t: srect.top    - crect.top,
10314								r: srect.right  - crect.left,
10315								b: srect.bottom - crect.top,
10316								w: srect.width,
10317								h: srect.height
10318							};
10319							
10320							canConstrainHorz = hadAutoPano || clipDim[1] >= selfDim[1] * mx[3] && clipDim[0] >= selfDim[0] * mx[0];
10321							//log('wh: ' + clipDim[1] + ' ih: ' + selfDim[1] + ' sc: ' + mx[3].toFixed(2));
10322							allowNavigation = 	!(options.avoidNavigationOnZoomed && (clipDim[1] < selfDim[1] * mx[3] || clipDim[0] < selfDim[0] * mx[0]))
10323												|| 
10324												canConstrainHorz;
10325							
10326							if (canConstrainHorz) {
10327								dontSwipeUp = typeof options.onSwipedUp !== FUNCTION;
10328								dontSwipeDown = typeof options.onSwipedDown !== FUNCTION;
10329							} else {
10330								dontSwipeUp = dontSwipeDown = false;
10331							}
10332							
10333							registerEvents(e);
10334							handleSwipeStart(e);
10335							swipeHandled = false;
10336							// Do not cancel long tap
10337							return e.type === 'touchstart';
10338						},
10339					
10340					// Moving in progress
10341					swipeMove = function(e) {
10342							//log('swipeMove:' + e.type + ' [' + e.originalEvent.touches.length + ']');
10343							
10344							if (hasEnded || nonSwipeable ||
10345								e.originalEvent.touches && e.originalEvent.touches.length > 2) {
10346								// not active, prohibited area or three finger touch
10347								return true;
10348							}
10349							
10350							e.preventDefault();
10351							
10352							var lastX = currPos? currPos[0] : 0,   // Saving previous position as values
10353    							lastY = currPos? currPos[1] : 0,
10354								t = new Date().getTime(),
10355								dt = (t - lastTime) || 1;
10356														
10357							currPos = getPos(e.originalEvent);
10358							
10359							if (startPos === null || startPos.length < currPos.length) {
10360								// No swipestart happened: init startPos / startD now 
10361								for (var i = startPos.length || 0; i < currPos.length; i++) {
10362									startPos[i] = currPos[i];
10363								}
10364								startD = getDistance(currPos);
10365							}
10366							
10367							if (dontSwipeUp && dontSwipeDown) {
10368								// Horizontal-only
10369								currPos[1] = startPos[1];
10370								// Speed: Smoothing with 2/3 : 1/3
10371								speed[0] = (speed[0] + 2 * (currPos[0] - lastX) / dt) / 3;
10372								
10373							} else {
10374								var dx = currPos[0] - startPos[0],
10375									dy = currPos[1] - startPos[1],
10376									d  = Math.abs(dx),
10377									a  = dy !== 0 ? Math.abs(dx / dy) : Infinity;
10378									
10379								// Can move both axes
10380								if (canConstrainHorz && (d > 30 || d > 8 && a > 0.25)) {
10381									// Change to horizontal-only swipe
10382									dontSwipeUp = dontSwipeDown = true;
10383								}
10384								
10385								if (dontSwipeUp && currPos[1] < startPos[1] ||
10386									dontSwipeDown && currPos[1] > startPos[1]) {
10387									currPos[1] = startPos[1];
10388								}
10389								
10390								speed = [
10391										(speed[0] + 2 * (currPos[0] - lastX) / dt) / 3,
10392										(speed[1] + 2 * (currPos[1] - lastY) / dt) / 3
10393									];
10394							}
10395							
10396							lastTime = t; 
10397							transformTo(currPos);
10398							return false;
10399						},
10400					
10401					// Cancel swipe (e.g. moved out of the screen)
10402					swipeCancel = function(e) {
10403							//log('swipeCancel:' + e.type);
10404							
10405							hasEnded = true;
10406							hasStarted = false;
10407							
10408							unregisterEvents(e);
10409							
10410							setTimeout(function() { 
10411									self.data({
10412											scrolling:	 	false,
10413											swipeEnded:		true
10414										});
10415								}, 100);
10416							
10417							if (startPos) {
10418								transformTo(startPos, null, 100);
10419							}
10420							
10421							return false;
10422						},
10423					
10424					// Stopped moving or clicked : mouse/touch up
10425					swipeEnd = function(e) {
10426							//log('swipeEnd:' + e.type);
10427							if (nonSwipeable) {
10428								// Prohibited area
10429								return true;
10430							}
10431							
10432							e.preventDefault();
10433							
10434							hasStarted = false;
10435							hasEnded = true;
10436							
10437							//currPos = getPos(e.originalEvent);
10438							
10439							if (currPos.length === 4) {
10440								handlePinchZoom(e);
10441							} else {
10442								var dx = currPos[0] - startPos[0],
10443									dy = currPos[1] - startPos[1],
10444									dt = new Date().getTime() - startTime,
10445									dist = Math.hypot(dx, dy),
10446									moveThreshold = Math.max(options.threshold, options.minSwipeFraction * clipDim[0]),
10447									w = selfDim[0] * scale * mx[0],
10448									h = selfDim[1] * scale * mx[3];
10449								
10450								if ((dt > 50) && (dist > moveThreshold)) {
10451									// Not a click, because it took longer or the distance is larger than the threshold
10452									
10453									// Setting data flags
10454									self.data({
10455											scrolling:	 	true,
10456											swipeEnded:		true,
10457											taplength:		dt
10458										});
10459									
10460									if (dontSwipeUp && dontSwipeDown) {
10461										// Constrain horizontally
10462										var extra = speed[0] * options.swipeoutDuration / 2,	// extra travel after end event
10463											slackX = options.overThreshold * clipDim[0],
10464											pL = panRect.l + dx + extra,					// prospective edges (container coords)
10465											pR = panRect.r + dx + extra;
10466										// Linear slowing: dx = v * t / 2;
10467										
10468										if (	(clipDim[0] >= panRect.w) ||
10469												(dx &&
10470													(
10471														(pL > slackX) ||						// gap opened on the left
10472														(pR < clipDim[0] - slackX)				// gap opened on the right
10473													)
10474												)
10475											) {
10476											// Swiped => move and fade out
10477											transformTo([ currPos[0] + extra, startPos[1] ], 0, options.swipeoutDuration, handleFinished);
10478											
10479											if (allowNavigation) {
10480												if (dx > 0) {
10481													handleSwipedRight(e);
10482												} else {
10483													handleSwipedLeft(e);
10484												}
10485											}
10486											
10487										} else {
10488											// Panoramic :: Hasn't reached the edge yet
10489											transformTo([ currPos[0] + extra, startPos[1] ], null, options.swipeoutDuration);
10490											unregisterEvents(e);
10491										}
10492									
10493									} else {
10494										// No constrain
10495										var vertical = Math.abs(dy) > 1.5 * Math.abs(dx), 				// max 30 degrees
10496											extra = [													// extra = extra travel after end event
10497													speed[0] * options.swipeoutDuration / 2,
10498													speed[1] * options.swipeoutDuration / 2
10499												],
10500											off = [ 0, 0 ],												// Amount of moving over the boundaries
10501												// Prospective image edges in container coords after this gesture
10502												slackX = options.overThreshold * clipDim[0],
10503												slackY = options.overThreshold * clipDim[1],
10504												pL = panRect.l + dx + extra[0],
10505												pR = panRect.r + dx + extra[0],
10506												pT = panRect.t + dy + extra[1],
10507												pB = panRect.b + dy + extra[1];
10508											
10509										// off = how far an edge has crossed its allowed boundary (incl. slack).
10510										// Only meaningful when the image is larger than the container on that
10511										// axis; smaller images are treated as swipes further down.
10512										if (panRect.w > clipDim[0]) {
10513											if (pL > slackX) {
10514												off[0] = pL - slackX;							// gap on the left
10515											} else if (pR < clipDim[0] - slackX) {
10516												off[0] = pR - (clipDim[0] - slackX);		// gap on the right
10517											}
10518										}
10519										if (panRect.h > clipDim[1]) {
10520											if (pT > slackY) {
10521												off[1] = pT - slackY;							// gap on the top
10522											} else if (pB < clipDim[1] - slackY) {
10523												off[1] = pB - (clipDim[1] - slackY);		// gap on the bottom
10524											}
10525										}
10526										
10527										if (w < clipDim[0] * 1.1 || h < clipDim[1] * 1.1) {
10528											// Smaller than screen at least in one direction and swiped over the boundaries
10529											if (allowNavigation) {
10530												if (typeof options.onSwipedDown === FUNCTION && vertical && dy > 0) {
10531													// Swiped down
10532													transformTo([ startPos[0], currPos[1] + extra[1] ], 0, options.swipeoutDuration, handleFinished);
10533													handleSwipedDown(e);
10534												} else if (typeof options.onSwipedUp === FUNCTION && vertical && dy < 0) {
10535													// Swiped up
10536													transformTo([ startPos[0], currPos[1] + extra[1] ], 0, options.swipeoutDuration, handleFinished);
10537													handleSwipedUp(e);
10538												} else {
10539													// Horizontal swipe or vertical but no up or down swipe handler
10540													transformTo([ currPos[0] + extra[0], startPos[1] ], 0, options.swipeoutDuration, handleFinished);
10541													if (dx > 0) {
10542														handleSwipedRight(e);
10543													} else {
10544														handleSwipedLeft(e);
10545													}
10546												}
10547											} else {
10548												transformTo([ currPos[0] + extra[0] - off[0], currPos[1] + extra[1] - off[1] ], 1, options.swipeoutDuration);
10549												unregisterEvents(e);
10550											}
10551											
10552										} else {
10553											if (off[0] || off[1]) {
10554												if (allowNavigation) {
10555													if (typeof options.onSwipedDown === FUNCTION && vertical && off[1] > clipDim[1] / 2) {
10556														// Swiped down
10557														transformTo([ startPos[0], currPos[1] + extra[1] ], 0, options.swipeoutDuration, handleFinished);
10558														handleSwipedDown(e);
10559													} else if (typeof options.onSwipedUp === FUNCTION && vertical && (-off[1]) > clipDim[1] / 2) {
10560														// Swiped up
10561														transformTo([ startPos[0], currPos[1] + extra[1] ], 0, options.swipeoutDuration, handleFinished);
10562														handleSwipedUp(e);
10563													} else if (Math.abs(off[0]) > clipDim[0] / 2) {
10564														// Horizontal swipe or vertical but no up or down swipe handler
10565														transformTo([ currPos[0] + extra[0], startPos[1] ], 0, options.swipeoutDuration, handleFinished);
10566														if (dx > 0) {
10567															handleSwipedRight(e);
10568														} else {
10569															handleSwipedLeft(e);
10570														}
10571													} else {
10572														// Rebounce to boundary
10573														transformTo([ currPos[0] + extra[0] - off[0], currPos[1] + extra[1] - off[1] ], null, options.swipeoutDuration);
10574														unregisterEvents(e);
10575													}														
10576												} else {
10577													// Rebounce to boundary
10578													transformTo([ currPos[0] + extra[0] - off[0], currPos[1] + extra[1] - off[1] ], null, options.swipeoutDuration);
10579													unregisterEvents(e);
10580												}
10581												
10582											} else {
10583												// No need to limit; continue swiping
10584												transformTo([ currPos[0] + extra[0], currPos[1] + extra[1] ], null, options.swipeoutDuration);
10585												unregisterEvents(e);
10586											}
10587										}
10588									}	
10589								
10590									clickedOnce = false;
10591								
10592								} else if (dist > options.tapTolerance) {
10593									// Moved more than a tap but not far enough to navigate:
10594									// rebound, and don't trigger a click or navigation
10595									if (chkDoubleClick) {
10596										clearTimeout(chkDoubleClick);
10597										chkDoubleClick = null;
10598									}
10599									clickedOnce = false;
10600									handleSmallSwipe(e);
10601									transformTo(startPos, null, 100);
10602									
10603								} else if (options.onDoubleClick && clickedOnce) {
10604									// Second clean tap inside the window: double-tap.
10605									// (clickedOnce is only still true if the single-click
10606									// timer hasn't fired, i.e. we're within doubleClickThreshold.)
10607									clickedOnce = false;
10608									handleDoubleClick(e);
10609									
10610								} else if (options.onDoubleClick) {
10611									// First clean tap: tear the listeners down and wait to see
10612									// whether a second tap follows before firing a single click
10613									clickedOnce = true;
10614									unregisterEvents(e);
10615									chkDoubleClick = setTimeout(function() {
10616											chkDoubleClick = null;
10617											clickedOnce = false;
10618											handleClick(e);
10619										}, options.doubleClickThreshold);
10620									
10621								} else {
10622									// No double-click handler: fire the single click immediately
10623									handleClick(e);
10624								}
10625							}
10626							
10627							return false;
10628						},
10629					
10630					noAction = function(e) {
10631							e.preventDefault();
10632							return false;
10633						},
10634					
10635					// Tearing off the swipe handler
10636					removeSwipe = function() {
10637							setTimeout(function() { 
10638									self.data('scrolling', false);
10639								}, 20);
10640							self.removeAttr('draggable');
10641							self.add($(document)).off('.' + ns);
10642						};
10643					
10644				// Starts here
10645				ns = self.data('lsw_ns');
10646				
10647				if (ns) {
10648					// Clean up events that may hang around (in theory this should never happen)
10649					self.add($(document)).off('.' + ns);
10650				}
10651				
10652				// Storing new namespace
10653				self.data('lsw_ns', ns = getUniqueId('lsw'));
10654				
10655				// Need to be updated about the window size
10656				$(window).on('resize.' + ns, function() {
10657						clearTimeout(rto);
10658						rto = setTimeout(function() {
10659							clipDim = getDim(cont);		
10660						}, 50);
10661					});
10662				
10663				self.attr('draggable', 'false')
10664					.on(TOUCH.START + '.' + ns + ' dragstart.' + ns + ' mousedown.' + ns, swipeStart)
10665					//.on(TOUCH.CANCEL + '.' + ns + ' dragcancel.' + ns, swipeCancel)
10666					.on(TOUCH.MOVE + '.' + ns + ' drag.' + ns, swipeMove)
10667					// External remove request
10668					.on('removeSwipe.' + ns, removeSwipe)
10669					// Do not select
10670					.on('selectstart.' + ns, noAction);
10671			});
10672		};
10673			
10674})(jQuery);
10675/*
10676 * 	laza.autopano.js - auto panorama viewer
10677 *	jslint browser: true
10678 */
10679 
10680;(function($, $document) {	
10681	'use strict';
10682	// Requires laza.util
10683
10684	$.fn.autopano = function(settings) {
10685		
10686			settings = $.extend({}, $.fn.autopano.defaults, settings);
10687	
10688			var self = $(this),							// Element
10689				cont = self.parent(),					// Container
10690				exposedStart = false,					// Left end reached
10691				exposedEnd = false,						// Right end reached
10692				direction = 0,							// Current direction
10693				maxDur = 1600 / settings.speed,			// Max. duration (40s - 20s - 10s)
10694				minDur = 120 / settings.speed,			// Min. duration (3s - 1.5s - 0.75s)
10695				size, 									// Element size
10696				screenSize,								// Container size
10697				vertical = false,						// Move axis
10698				rto,									// Resize timeout
10699				ns = self.data('lap_ns'),				// Name space
10700					
10701				// Initializing
10702				
10703				init = function() {
10704						
10705						// Updating window size
10706						
10707						$(window).on('resize.' + ns, function() {
10708								clearTimeout(rto);
10709								rto = setTimeout(updateDims, 50, false);
10710								return true;
10711							});
10712				
10713						// External events
10714						
10715						self.data('hasAutoPano', true)
10716							.on('autopanoRemove.' + ns, function() {
10717								direction = 0;
10718								remove();
10719								return false;
10720							})
10721							.on('autopanoStart.' + ns, function() {
10722								startAuto();
10723								return false;
10724							})
10725							.on('autopanoStop.' + ns, function() {
10726								direction = 0;
10727								stopMove();
10728								return false;
10729							})
10730							.on('autopanoAdjust', function() {
10731								updateDims(false);
10732							});
10733						
10734						updateDims(true);
10735					},
10736				
10737				// Update dimensions
10738				
10739				updateDims = function(first) {
10740						var mx = self.getMatrix(), 
10741							ew = self[0].scrollWidth,
10742							eh = self[0].scrollHeight,
10743							cw = cont[0].clientWidth,
10744							ch = cont[0].clientHeight;
10745							
10746						if (ew / cw < eh / ch) {
10747							// Vertical
10748							size = eh;
10749							screenSize = ch;
10750							if (!vertical) {
10751								// Changed direction
10752								vertical = true;
10753								if (!first) {
10754									mx[4] = - ew / 2;
10755									self.css({
10756											transition: 	'none',
10757											transform:		'matrix(' + mx.join(',') + ')'
10758										});
10759								}
10760							}
10761						} else {
10762							// Horizontal
10763							size = ew;
10764							screenSize = cw;
10765							if (vertical) {
10766								// Changed direction
10767								vertical = false;
10768								if (!first) {
10769									mx[5] = - eh / 2;
10770									self.css({
10771											transition: 	'none',
10772											transform:		'matrix(' + mx.join(',') + ')'
10773										});
10774								}
10775							}
10776						}
10777						
10778						if (size > screenSize) {
10779							//console.log('Dimensions updated');
10780							if (first || !direction) {
10781								// Start if wider/taller than screen
10782								setTimeout(startAuto, 100);
10783							} else if (direction > 0) {
10784								moveToEnd();
10785							} else {
10786								moveToStart();
10787							}
10788						} else {
10789							// Stop if smaller
10790							stopMove();
10791						}
10792					},
10793				
10794				// Remove autopano
10795				
10796				remove = function() {
10797					
10798						stopMove();
10799						
10800						setTimeout(function() { 
10801								cont.data('panomove', false);
10802							}, 20);
10803						
10804						self.removeAttr('draggable');
10805						self.add($document).off('.' + ns);
10806					},
10807					
10808				// Reached the start
10809				
10810				reachedStart = function(e) {
10811						//eventLog(e);
10812						exposedStart = true;
10813						
10814						if (direction) {
10815							if (!exposedEnd || settings.loop) {
10816								moveToEnd();
10817							} else {
10818								stopMove();
10819							}
10820						}
10821					},
10822					
10823				// Reached the end
10824				
10825				reachedEnd = function(e) {
10826						//eventLog(e);
10827						exposedEnd = true;
10828						
10829						if (direction) {
10830							if (!exposedStart || settings.loop) {
10831								moveToStart();
10832							} else {
10833								stopMove();
10834							}
10835						}
10836					},
10837					
10838				// Move to left end
10839				
10840				moveToStart = function() {
10841						var dis = - screenSize / 2,
10842							mx = self.getMatrix(),
10843							dur;
10844							
10845						if (vertical) {
10846							dur = 1000 * Math.minMax(minDur, Math.abs(mx[5] - dis) / settings.speed, maxDur),
10847							mx[5] = dis;
10848						} else {
10849							dur = 1000 * Math.minMax(minDur, Math.abs(mx[4] - dis) / settings.speed, maxDur),
10850							mx[4] = dis;
10851						}
10852						
10853						exposedEnd = false;
10854						direction = -1;
10855						self[0].style.transition = 'none'; 
10856						self.one('transitionend', reachedStart)
10857							.css({
10858								transition:		'transform ' + dur + 'ms ' + settings.easing,
10859								transform:		'matrix(' + mx.join(',') + ')'
10860							});	
10861					},
10862				
10863				// Move to right end
10864				
10865				moveToEnd = function() {
10866						var dis = screenSize / 2 - size,
10867							mx = self.getMatrix(),
10868							dur;
10869						
10870						if (vertical) {
10871							dur = 1000 * Math.minMax(minDur, Math.abs(mx[5] - dis) / settings.speed, maxDur),
10872							mx[5] = dis;
10873						} else {
10874							dur = 1000 * Math.minMax(minDur, Math.abs(mx[4] - dis) / settings.speed, maxDur),
10875							mx[4] = dis;
10876						}
10877						
10878						exposedStart = false;
10879						direction = 1;
10880						self[0].style.transition = 'none'; 
10881						self.one('transitionend', reachedEnd)
10882							.css({
10883								transition:		'transform ' + dur + 'ms ' + settings.easing,
10884								transform:		'matrix(' + mx.join(',') + ')'
10885							});
10886					},
10887					
10888				// Start auto pano
10889				
10890				startAuto = function() {
10891						var mx = self.getMatrix(),
10892							c = vertical? (self[0].scrollHeight / 2 + mx[5]) : (self[0].scrollWidth / 2 + mx[4]);
10893						
10894						if (settings.direction < 0 || (settings.direction === 0 && c > 0)) {
10895							moveToStart();
10896						} else {
10897							moveToEnd();
10898						}
10899						
10900						cont.addClass(settings.panoMoveClass);
10901						cont.data('panomove', true);	
10902					},
10903				
10904				// Stop moving
10905				
10906				stopMove = function() {
10907						var mx = self.getMatrix();
10908						
10909						direction = 0;
10910						self.off('transitionend'); 
10911						
10912						// Move immediately
10913						self.css({
10914								transition:		'none',
10915								transform:		'matrix(' + mx.join(',') + ')'
10916							});
10917						
10918						cont.data('panomove', false);
10919						cont.removeClass(settings.panoMoveClass);
10920					};
10921				
10922	
10923			// Initializing
10924						
10925			if (ns) {
10926				$(window).add($(document)).off('.' + ns);
10927			}
10928			
10929			// Creating new namespace
10930			
10931			self.data('lap_ns', ns = 'lap_' + ($.fn.autopano._counter = ($.fn.autopano._counter || 0) + 1));
10932			
10933			// Initializing
10934			
10935			init();
10936			
10937			// Ready event
10938			
10939			if (settings.hasOwnProperty('onReady') && typeof settings.onReady === FUNCTION) {
10940				settings.onReady(self[0]);
10941			}
10942			
10943			return this;
10944		};
10945	
10946	$.fn.autopano.defaults = {
10947			direction:			-1,									// 1: to right, -1: to left
10948			speed:				80,									// pixel / s
10949			easing:				'cubic-bezier(0.2, 0, 0.8, 1)',		// Easing function
10950			autoStart:			true,								// Auto start
10951			loop:				true,								// continue after exposed both ends
10952			panoMoveClass:		'pano-move'							// Class for the container during move
10953		};
10954	
10955})(jQuery, jQuery(document));
10956/*	
10957 *	audioPlayer() :: adds an audio player to a button
10958 *
10959 *	Copyright by Lazaworx
10960 *	http://www.lazaworx.com
10961 *	Author: Laszlo Molnar
10962 *
10963 *	Dual licensed under the MIT and GPL licenses.
10964 *	- http://www.opensource.org/licenses/mit-license.php
10965 *	- http://www.gnu.org/copyleft/gpl.html
10966 *
10967 *	Usage: $(element).addAudioPlayer(options);
10968 *	options:
10969 		autoPlay:	false,
10970 		loop:		true,
10971 		volume:		0.2,
10972 		target:		'id'		// or as tooltip if no target
10973 		src:		''			// or pulled from data-src '["song1.mp3","song2.mp3"]'
10974 		
10975 */
10976
10977;(function($) {
10978	'use strict';
10979	
10980	$.fn.audioPlayer = function(settings) {
10981		
10982			if ($(this).data('audioplayer')) {
10983				settings = $.extend({}, settings, $(this).data('audioplayer'));
10984			}
10985			
10986			settings = $.extend({}, $.fn.audioPlayer.defaults, settings);
10987			
10988			if (!settings.hasOwnProperty('rootPath') || settings.rootPath === '.') {
10989				settings.rootPath = '';
10990			}
10991			
10992			const ns = 'lap_ns',
10993				musicFolder = (settings.rootPath? (settings.rootPath + '/') : '') + settings.folder + '/',
10994				ACTIVE = 'active',
10995				PAUSED = 'paused';
10996					
10997			return this.each(function() {
10998					let self = $(this),
10999						target = $(),
11000						player = $(),
11001						playList = $(),
11002						audio = $(),
11003						source = $(),
11004						curr = 0,
11005						songs,
11006						promise,
11007						savedVolume = settings.volume,
11008						fadeTimeout,
11009						fadeInOn = false,
11010						fadeOutOn = false,
11011						
11012					// Creating player
11013					
11014					createPlayer = function() {
11015							
11016							if (settings.hasOwnProperty('target')) {
11017								player = $('#' + settings.target);
11018							} else {
11019								player = $('<div>', {
11020										'class':	settings.playerHook
11021									});
11022							}
11023									
11024							audio = $('<audio>', {
11025									preload: 		'auto',
11026									controls:		true,
11027									controlsList:	'nofullscreen nodownload noremote'
11028								}).appendTo(player);
11029							
11030							audio[0].pause();
11031							self.data(PAUSED, true);
11032								
11033							source = $('<source>', {
11034									src: 		musicFolder + songs[curr] 	
11035								}).appendTo(audio);
11036							
11037							setTimeout(function() {
11038									
11039									// Wiring events with a delay
11040									audio.on('ended.' + ns, function() {
11041											playNext();
11042											return true;
11043										});
11044									
11045									audio.on('pause.' + ns, function() {
11046												self.data(PAUSED, true);
11047												self.removeClass(ACTIVE);
11048												if (typeof settings['onPaused'] === FUNCTION) {
11049													settings.onPaused.call(self);
11050												}
11051											});
11052										
11053									audio.on('playing.' + ns, function() {
11054											self.data(PAUSED, false);
11055											if (audio[0].volume === 0) {
11056												audio[0].volume = savedVolume || settings.volume;
11057											}
11058											self.addClass(ACTIVE);
11059											if (typeof settings['onPlaying'] === FUNCTION) {
11060												settings.onPlaying.call(self);
11061											}
11062										});
11063									
11064									audio.on('volumechange.' + ns, function() {
11065											if (!fadeInOn && !fadeOutOn) {
11066												//clearTimeout(fadeTimeout);
11067												savedVolume = audio[0]['volume'];
11068											}
11069										});
11070								}, 100);
11071								
11072							if (songs.length) {
11073								playList = $('<ul>', {
11074										'class':		settings.playListHook
11075									}).appendTo(player);
11076		
11077								for (var i = 0; i < songs.length; i++) {
11078									playList.append($('<li>').append($('<a>', {
11079											text: 		decodeURIComponent(songs[i].replace(/\.[^\.]*$/, ''))
11080										}).on('click.' + ns, function(e) {
11081											var i = playList.children().index($(e.target).parent());
11082											play(i);
11083											return false;
11084										})));
11085								}
11086								
11087								playList.children().eq(curr).addClass(ACTIVE);
11088							}
11089						},
11090						
11091					// Setting the current song's source
11092					
11093					setSource = function(n) {
11094						
11095							audio[0].pause();
11096							
11097							if (n >= songs.length || n < 0) {
11098								n = ((n % songs.length) + songs.length) % songs.length;
11099							}
11100							
11101							source.attr({
11102									src: 	musicFolder + songs[n]
11103								})
11104							
11105							audio[0].load();
11106							
11107							curr = n;
11108							
11109							playList.children().removeClass(ACTIVE);
11110							playList.children().eq(n).addClass(ACTIVE);
11111						},
11112						
11113					// Start playing
11114					
11115					play = function(n) {
11116							if (typeof n === UNDEF || n instanceof $.Event) {
11117								n = curr;
11118							}
11119							
11120							if (!source.attr('src') || n !== curr) {
11121								// New/missing song
11122								setSource(n);
11123							} else if (!audio[0].paused) {
11124								// Playing already
11125								return;
11126							}
11127							
11128							if (promise = audio[0].play()) {
11129								promise.then(function() {
11130										// Autoplay started!
11131										self.addClass(ACTIVE);
11132										self.data(PAUSED, false);
11133										if (typeof settings['onPlaying'] === FUNCTION) {
11134											settings.onPlaying.call(self);
11135										}
11136									}, function(err) {
11137										// Autoplay was prevented.
11138										// Show a "Play" button so that user can start playback.
11139										self.removeClass(ACTIVE);
11140										self.data(PAUSED, true);
11141										if (typeof settings['onPaused'] === FUNCTION) {
11142											settings.onPaused.call(self);
11143										}
11144										console.log('Autoplay has been prevented by the browser. Interact with the page first!');
11145									});
11146							}
11147						},
11148							
11149					// Pausing play
11150					
11151					pause = function() {
11152							audio[0].pause();
11153							promise = null;
11154							self.removeClass(ACTIVE);
11155							self.data(PAUSED, true);
11156						},
11157					
11158					// Toggling play
11159					
11160					toggle = function() {
11161							if (audio[0].paused) {
11162								play();
11163							} else {
11164								pause();
11165							}
11166						},
11167				
11168						// Fade in one step
11169						
11170					fadeInStep = function() {
11171						
11172							clearTimeout(fadeTimeout);
11173							
11174							let v = audio[0]['volume'];
11175							
11176							if (typeof v !== 'number') {
11177								v = parseFloat(v);
11178								if (typeof v !== 'number') {
11179									return;
11180								}
11181							}
11182							
11183							v = Math.min(v + settings.step, 1);
11184							
11185							if (v >= savedVolume) {
11186								audio[0]['volume'] = savedVolume;
11187								setTimeout(function() {
11188									fadeInOn = false;
11189									}, 50);
11190								return;
11191							}
11192								
11193							audio[0]['volume'] = v;
11194							
11195							fadeTimeout = setTimeout(fadeInStep, settings.stepFreq);
11196						},
11197							
11198					// Fade in playing
11199					
11200					fadeIn = function() {
11201						
11202							if (audio[0].paused) {
11203								audio[0]['volume'] = 0.01;
11204								play();
11205							}
11206								
11207							fadeInOn = true;
11208							fadeOutOn = false;
11209							fadeTimeout = setTimeout(fadeInStep, settings.stepFreq);
11210								
11211							self.data(PAUSED, false);
11212						},
11213							
11214					// Fade out one step
11215					
11216					fadeOutStep = function() {
11217						
11218							clearTimeout(fadeTimeout);
11219							
11220							let v = audio[0]['volume'];
11221							
11222							if (typeof v !== 'number') {
11223								v = parseFloat(v);
11224								if (typeof v !== 'number') {
11225									return;
11226								}
11227							}
11228							
11229							v = Math.max(v - settings.step, 0);
11230							
11231							if (v < 0.01) {
11232								audio[0]['volume'] = 0;
11233								pause();
11234								setTimeout(function() {
11235										fadeOutOn = false;
11236									}, 50);
11237								return;
11238							}
11239							
11240							audio[0]['volume'] = v;
11241								
11242							fadeTimeout = setTimeout(fadeOutStep, settings.stepFreq);
11243						},
11244						
11245					// Fade out playing
11246					
11247					fadeOut = function() {
11248						
11249							if (!audio[0].paused) {
11250								fadeOutOn = true;
11251								fadeInOn = false;
11252								fadeTimeout = setTimeout(fadeOutStep, settings.stepFreq);
11253							}
11254							
11255							self.data(PAUSED, true);
11256						},
11257							
11258					// Fade out playing
11259					
11260					fadeToggle = function() {
11261							
11262							if (fadeInOn || !audio[0].paused) {
11263								fadeOut();
11264							} else {
11265								fadeIn();
11266							}
11267						},
11268					
11269						// Next song
11270					
11271					playNext = function() {
11272							if (curr >= songs.length - 1) {
11273								if (settings.loop) {
11274									play(0);
11275								}
11276							} else {
11277								play(curr + 1);
11278							}
11279						},
11280					
11281					// Destroying player
11282					
11283					destroy = function() {
11284							player.trigger('removeTooltip');
11285							player.add(self).off('.' + ns);
11286							player.remove();
11287						},
11288					
11289					// Saving play status
11290				
11291					saveStatus = function() {
11292							if ($.cookie) {						
11293								$.cookie('ap-status', 
11294									(audio[0].paused? '0':'1') + 
11295									'::' + (audio[0].currentTime || 0) + 
11296									'::' + ((fadeInOn || fadeOutOn || audio[0].paused)? savedVolume : audio[0].volume) +
11297									(curr? ('::' + curr) : '')
11298								);
11299							}
11300						},
11301					
11302					// Loading play status
11303					
11304					loadStatus = function() {
11305							if ($.cookie) {
11306								let c = $.cookie('ap-status');
11307								
11308								if (c) {
11309									c = c.split('::');
11310									
11311									settings.autoPlay = (c[0] === '1');
11312									settings.startTime = parseFloat(c[1] || 0);
11313									savedVolume = settings.volume = parseFloat(c[2]) || .2;
11314									curr = parseInt(c[3] || 0, 10);
11315									
11316									if (curr < 0 || curr > songs.length) {
11317										curr = 0;
11318									}
11319								}
11320							}
11321						};
11322							
11323				
11324				// Loading playlist 
11325				if (self.data('src')) {
11326					songs = self.data('src').split('::');
11327				} else if (settings.hasOwnProperty('src')) {
11328					songs = (typeof settings.src === 'string')? settings.src.split('::') : settings.src;
11329				} else {
11330					return;
11331				}
11332				
11333				// Load status to initialize, save status on unload
11334				if (settings.saveStatus) {
11335					loadStatus();
11336				}
11337				
11338				// Creating the AUDIO element
11339				createPlayer();
11340				
11341				// If no target, add as a tooltip, playlist appears on hover 
11342				if (settings.hasOwnProperty('target')) {
11343					target = $('#' + self.data('rel'));
11344				}
11345				
11346				if (target.length) {
11347					target.append(player);
11348				} else {
11349					self.addTooltip(player, {
11350							pos:		settings.pos,
11351							delay:		500
11352						});
11353				}
11354					
11355				// Play/Pause button functionality 
11356				self.on('click.' + ns, function() {
11357						if (!target.length || target.is(':hidden')) {
11358							if (self.data(PAUSED)) {
11359								fadeIn();
11360							} else {
11361								fadeOut();
11362							}
11363							return false;
11364						} else {
11365							return true;
11366						}
11367					});
11368				
11369				// Events for outside control
11370				
11371				self.on('removePlayer.' + ns, destroy);
11372				
11373				self.on('startPlayer.' + ns, play);
11374				
11375				self.on('pausePlayer.' + ns, pause);
11376				
11377				self.on('togglePlayer.' + ns, toggle);
11378				
11379				self.on('fadeInPlayer.' + ns, fadeIn);
11380				
11381				self.on('fadeOutPlayer.' + ns, fadeOut);
11382	
11383				self.on('fadeTogglePlayer.' + ns, fadeToggle);
11384
11385				self.data('slideshowControl', settings.slideshowControl);
11386		
11387				// Delaying the player initialization for the sake of Firefox
11388				
11389				setTimeout(function() {
11390					
11391					audio[0]['volume'] = settings.volume;
11392					
11393					if (settings.hasOwnProperty('startTime')) {
11394						audio[0]['currentTime'] = settings.startTime;
11395					}						
11396	
11397					// Start playing if Auto on
11398					if (settings.autoPlay) {
11399						fadeIn();
11400					}
11401					
11402					$(window).on('pagehide.' + ns, saveStatus);
11403					
11404				}, 50);
11405				
11406			});
11407			
11408		};
11409	
11410	$.fn.audioPlayer.defaults = {
11411			autoPlay:			true,
11412			loop:				true,
11413			volume:				0.25,
11414			stepFreq:			30,
11415			step:				.02,
11416			saveStatus:			true,
11417			playerHook: 		'player',
11418			playListHook:		'play-list',
11419			folder:				'res',
11420			rootPath:			'',
11421			pos:				[1,2,1,0],
11422			onPlaying:			null,
11423			onPaused:			null
11424		};
11425	
11426})(jQuery);
11427/*
11428 * 	lightbox.js - lightbox for Tiger skin
11429 *	jslint browser: true
11430 */
11431 
11432;(function($, $window, $document) {	
11433	'use strict';
11434	// Requires laza.util, laza.swipe and laza.transform
11435	var $body = jQuery('body');
11436
11437	$.fn.lightbox = function(album, settings) {
11438		
11439			if (typeof album === UNDEF) {
11440				return this;
11441			}
11442			
11443			settings = $.extend({}, $.fn.lightbox.defaults, settings);
11444	
11445			var self = $(this),
11446				text = getTranslations($.fn.lightbox.text),
11447				thumbs = $([]),
11448				thumb = $(),
11449				main = $(),
11450				oldMain = $(),
11451				image = $(),
11452				media = $(),
11453				caption = $(),
11454				oldCaption = $(),
11455				controls = $(),
11456				audioClip = $(),
11457				el,
11458				isImage = true,
11459				isVideo = false,
11460				isAudio = false,
11461				isOther = false,
11462				isWebPage = false,
11463				isExternal = false,
11464				isVr = false,
11465				isPdf = false,
11466				isPanorama = false,
11467				dontRemove = false,
11468				slideshow = null,
11469				slideshowSuspended = false,
11470				slideshowDelay = settings.slideshowDelay,
11471				transitionSpeed = settings.transitionSpeed,
11472				audioClipTimeout = null,
11473				fitTimeout = null,
11474				controlsTimeout = null,
11475				resizeTimeout = null,
11476				progressTo = null,
11477				listening = false,
11478				
11479				maxZoom = settings.maxZoom,
11480				fitImages = settings.fitImages,
11481				zoomLevel,
11482				
11483				thumbsVisibleDefault = settings.thumbsVisible,
11484				captionVisibleDefault = settings.captionVisible,
11485				
11486				curr = -1,
11487				loadCounter = 0,
11488				loadTimer,
11489				connectionSpeed = 0,
11490				direction = 0,
11491				imgPadding,
11492				lbPadding = settings['fitPadding'], 
11493				
11494				// Elements
11495				lightbox,
11496				lb_overlay,
11497				lb_activity,
11498				
11499				// Thumbstrip
11500				lb_thumbstrip,
11501				lb_thumb_cont,
11502				lb_thumbs,
11503				
11504				// Controls
11505				lb_btn_left,
11506				lb_up,
11507				lb_zoom,
11508				lb_zoomin,
11509				lb_zoomout,
11510				lb_zoom_slider,
11511				lb_fullscr,
11512				lb_exitfullscr,
11513				lb_showthumbs,
11514				lb_hidethumbs,
11515				lb_showcaption,
11516				lb_hidecaption,
11517				lb_play_pause,
11518				lb_progress,
11519				lb_timing_value,
11520				lb_btn_right,
11521								
11522				ns = self.data('llb_ns'),
11523				extraSizes = settings.hasOwnProperty('extraSizes')? settings.extraSizes.split(/,\s*/) : false,
11524				
11525				backgroundAudioRoot = $('[data-audioplayer]'),
11526	
11527				cleanup = function(ns) {	
11528						// cleaning up the old event handlers
11529						$window.add(document).off('.' + ns);
11530						// Removing the lightbox :: you can only have one lightbox per thumb set
11531						$('#' + ns).remove();
11532					},
11533				
11534				closeModal = function() {
11535					
11536						lightbox.find('.modal').trigger('destroy');
11537					},
11538							
11539				getThumbsHeight = function() {
11540						return (settings.fitBetween !== 'screen' && 
11541								settings.thumbsVisible && 
11542								thumbs.length > 1 && 
11543								lb_thumb_cont && 
11544								lb_thumb_cont.length)? 
11545							lb_thumb_cont[0].clientHeight : 0;
11546					},
11547				
11548				getInfoHeight = function() {
11549					
11550						return (settings.fitBetween !== 'screen' && 
11551								lightbox.hasClass(settings.captionVisibleClass)	&& 
11552								caption && 
11553								caption.length)? 
11554							caption[0].offsetHeight : 0;
11555					},
11556				
11557				getControlsHeight = function() {
11558					
11559						return controls[0].clientHeight;
11560					},
11561				/*
11562				isTapOnMediaControls = function(e, m) {
11563						
11564						var rect = m.getBoundingClientRect(),
11565							c = getCoords(e),
11566							top = rect.top + $window.scrollTop(),
11567							left = rect.left + $window.scrollLeft(),
11568							bottomBand = Math.max(40, rect.height * 0.18);
11569						
11570						// Bottom control bar
11571						if (c.y >= top + rect.height - bottomBand) {
11572							return true;
11573						}
11574						
11575						// Center play button, pre-first-play only (Safari/WebKit)
11576						if (m.paused && m.currentTime === 0) {
11577							var cw = Math.min(96, rect.width * 0.35),
11578								ch = Math.min(96, rect.height * 0.35),
11579								cx = left + rect.width / 2,
11580								cy = top + rect.height / 2;
11581							
11582							if (Math.abs(c.x - cx) <= cw / 2 && Math.abs(c.y - cy) <= ch / 2) {
11583								return true;
11584							}
11585						}
11586						
11587						return false;
11588					},
11589				*/
11590				// Fixing the zoom button after pinch
11591				fixZoom = function(zoom, coords) {
11592					
11593						if (settings.useZoom && settings.zoomSlider) {
11594							if (zoom === 1) {
11595								return;
11596							}
11597							
11598							var mnr = main.data('minRatio') || 1,
11599								mxr = main.data('maxRatio') || 1.4,
11600								z = mnr + zoomLevel * (mxr - mnr) / 100; 
11601							
11602							z = Math.minMax(mnr, z * zoom, mxr);
11603							zoomLevel = (z - mnr) / (mxr - mnr) * 100;
11604						
11605							setZoomSlider(zoom);
11606							setZoomBtn(zoomLevel < 50, zoomLevel >= 50);
11607						}
11608					},
11609				
11610				// Toggling zoom in simple zomm toggling
11611				toggleZoom = function(coords) {
11612					
11613						if (settings.useZoom && settings.zoomSlider) {
11614							fitImages = true;
11615							zoomLevel = (zoomLevel > 50)? 0 : 100;
11616							setZoomSlider(zoomLevel);
11617						} else {
11618							// Change fit strategy
11619							fitImages = !fitImages;
11620						}
11621						//savePrefs();
11622						if (typeof coords === UNDEF) {
11623							fitImage(true);
11624						} else {
11625							fitImage(true, coords);
11626						}
11627					},
11628					
11629				// Sets zoom (0 .. 100), where 100 means 1:1 size
11630				setZoom = function(zoom, coords) {
11631						//console.log('Set zoom:' + zoom);
11632						zoomLevel = zoom;
11633						setZoomSlider(zoom);
11634						
11635						if (typeof coords === UNDEF) {
11636							fitImage(true);
11637						} else {
11638							fitImage(true, coords);
11639						}
11640					},
11641					
11642				increaseZoom = function(incr, coords) {
11643						var v = settings.zoomSlider? getZoomSlider() : zoomLevel;
11644						
11645						if (v < 100) {
11646							v = Math.min(100, v + ((typeof incr !== UNDEF)? incr : 25));
11647						}
11648						
11649						if (typeof coords === UNDEF) {
11650							setZoom(v);
11651						} else {
11652							setZoom(v, coords);
11653						}
11654					},
11655						
11656				decreaseZoom = function(incr, coords) {
11657						var v = settings.zoomSlider? getZoomSlider() : zoomLevel;
11658						
11659						if (v > 0) {
11660							v = Math.max(0, v - ((typeof incr !== UNDEF)? incr : 25));
11661						}
11662						
11663						if (typeof coords === UNDEF) {
11664							setZoom(v);
11665						} else {
11666							setZoom(v, coords);
11667						}
11668					},
11669					
11670				getZoomSlider = function() {
11671						return parseInt(lb_zoom_slider.val());
11672					},
11673						
11674				setZoomSlider = function(zoom) {
11675						lb_zoom_slider.val(zoom);
11676					},
11677					
11678				setZoomBtn = function(canZoomIn, canZoomOut) {
11679					
11680						if (settings.zoomSlider) {
11681							if (canZoomIn || canZoomOut) {
11682								lightbox.addClass('show-zoom-level');
11683								if (zoomLevel === 0) {
11684									lb_zoom.removeClass('icon-zoom-out icon-zoom-level').addClass('icon-zoom-in');
11685								} else if (zoomLevel === 100) {
11686									lb_zoom.removeClass('icon-zoom-in icon-zoom-level').addClass('icon-zoom-out');
11687								} else {
11688									lb_zoom.removeClass('icon-zoom-in icon-zoom-out').addClass('icon-zoom-level');
11689								}
11690							} else {
11691								lightbox.removeClass('show-zoom-level');
11692							}
11693						} else {
11694							lightbox.toggleClass('show-zoom-out', canZoomOut);
11695							// Zoom out takes precedence if both possible
11696							lightbox.toggleClass('show-zoom-in', canZoomIn && !canZoomOut);
11697						}
11698						
11699					},
11700				
11701				// Slideshow timing controls
11702				
11703				setTiming = function(t) {
11704						lb_timing_value.html((t >= 1000)? (Math.round(t / 1000) + 's') : (t + 'ms')); 
11705					},
11706					
11707				increaseTiming = function() {
11708					
11709						if (settings.slideshowDelay > 1000) {
11710							settings.slideshowDelay += 1000;
11711						} else {
11712							settings.slideshowDelay += 100;
11713						}
11714						
11715						setTiming(settings.slideshowDelay);
11716					},
11717						
11718				decreaseTiming = function() {
11719					
11720						if (settings.slideshowDelay > 500) {
11721							if (settings.slideshowDelay > 1000) {
11722								settings.slideshowDelay -= 1000;
11723							} else {
11724								settings.slideshowDelay -= 100;
11725							}
11726						}
11727						
11728						setTiming(settings.slideshowDelay);
11729					},
11730					
11731				// Animating progress bar
11732				
11733				animateProgress = function(duration) {
11734						var d = 20 / duration,
11735						
11736							setProgress = function(to) {
11737								
11738									//progress.css('transform', 'rotate(' + to * 360 + 'deg)');
11739									lb_progress.css('width', to * 100 + '%');
11740
11741									if (to < 1) {
11742										to = Math.min(1, to + d);
11743										progressTo = setTimeout(setProgress, 20, to); 
11744									}
11745								};
11746							
11747						clearTimeout(progressTo);
11748						progressTo = setTimeout(setProgress, 20, 0);
11749					},
11750						
11751				showProgress = function() {
11752						if (lb_progress) {
11753							lb_progress.show();
11754							animateProgress(slideshowDelay);
11755						}
11756					},
11757				
11758				hideProgress = function() {
11759						if (lb_progress) {
11760							clearTimeout(progressTo);
11761							lb_progress.hide();
11762						}
11763					},
11764					
11765				getLightboxDims = function() {
11766						var lbDims = [ lightbox.width(), lightbox.height() ];
11767						
11768						return [ lbDims[0] - 2 * lbPadding, lbDims[1] - Math.max(getThumbsHeight(), lbPadding) - Math.max(getInfoHeight(), lbPadding) ];
11769					},
11770					
11771				// Reacalculating image position and size upon window resize
11772				
11773				fitImage = function(animate, center, doneFn) {
11774					
11775						if (!main || !main.length) {
11776							return;
11777						}
11778						
11779						var o = thumbs.eq(curr).data(J.OBJ),
11780							lbDims 			= [ lightbox.width(), lightbox.height() ],
11781							controlsHeight 	= (isVr || isOther || isExternal || (sett
11781ings.fitBetween === 'all' && !settings.autohideControls))? getControlsHeight() : 0,
11782							thumbsHeight 	= getThumbsHeight(),
11783							infoHeight 		= getInfoHeight(),
11784							
11785							checkPano = function(dims) {
11786									return settings.autoPano && (dims[0] / dims[1] > settings.horzPanoThreshold || dims[0] / dims[1] < settings.vertPanoThreshold);
11787								},
11788								
11789							checkPanoDims = function(dims) {
11790									return 	dims[0] / lbDims[0] >= settings.panoramaAutoDetect	 			// hangs over the edges least 1.3333 times the width or height
11791											||
11792											dims[1] / lbDims[1] >= settings.panoramaAutoDetect;
11793								},
11794								
11795							getTargetDims = function(dims, f) {
11796									return [ dims[0] * f, dims[1] * f ];
11797								},
11798								
11799							fitPano = function(dims) {
11800									return Math.max(
11801											(lbDims[0] - 2 * lbPadding - 2 * imgPadding) / dims[0],
11802											(lbDims[1] - Math.max(thumbsHeight + controlsHeight, lbPadding) - Math.max(infoHeight, lbPadding) - 2 * imgPadding) / dims[1]
11803										);
11804								},
11805								
11806							fitToLightbox = function(dims) {
11807									return Math.min(	
11808											(lbDims[0] - 2 * lbPadding - 2 * imgPadding) / dims[0], 
11809											(lbDims[1] - Math.max(thumbsHeight + controlsHeight, lbPadding) - Math.max(infoHeight, lbPadding) - 2 * imgPadding) / dims[1]
11810										);
11811								};
11812							
11813						// Image padding = border
11814						if (typeof imgPadding === UNDEF) {
11815							imgPadding = parseInt(main.css('paddingTop'));
11816						}
11817											
11818						// Lightbox padding
11819						if (typeof lbPadding === UNDEF) {
11820							lbPadding = 0;
11821						}
11822						
11823						// Set zoom level if undef
11824						if (typeof zoomLevel === UNDEF || zoomLevel === null) {
11825							zoomLevel = fitImages? 0 : 100;
11826							if (settings.useZoom && settings.zoomSlider) {
11827								setZoomSlider(zoomLevel);
11828							}
11829						}
11830						
11831						
11832						if (isVr || isOther || isWebPage || isExternal || isAudio && image.hasClass('default')) {
11833							// Non-zoomable types
11834							var th 	= Math.max(thumbsHeight + ((isVr || isOther || isExternal)? controlsHeight : 0), lbPadding),
11835								bh 	= Math.max(infoHeight, lbPadding),
11836								w	= lbDims[0] - 2 * lbPadding,
11837								h 	= lbDims[1] - (th + bh);
11838								
11839							if (isExternal || isAudio) {
11840								var imgDims = album.getDimensions(o);
11841								if (isExternal) {
11842									// Might be smaller than viewport
11843									if (imgDims[0] < w && imgDims[1] < h) {
11844										// Smaller than available space?
11845										w = Math.min(imgDims[0], w) + 2 * imgPadding;
11846										h = Math.min(imgDims[1], h) + 2 * imgPadding;
11847									}
11848								} else if (isAudio) {
11849									// Audio with no poster
11850									if (imgDims[0] > w || imgDims[1] > h) {
11851											// Default audio poster is larger than viewport
11852										var sc = Math.min(w / imgDims[0], h / imgDims[1]);
11853										w = imgDims[0] * sc;
11854										h = imgDims[1] * sc;
11855									} else {
11856										w = imgDims[0];
11857										h = imgDims[1];
11858									}
11859								}
11860							} else if (isWebPage) {
11861								w = Math.min(w, settings.maxPageWidth);
11862								h = 'auto';
11863							}
11864							
11865							main.data('fitted', true)
11866								.css({
11867									display:		'block',
11868									width:			w,
11869									height:			h,
11870									maxHeight:		'calc(100% - ' + (bh + th) + 'px)',
11871									transition:		'none',
11872									transform:		'matrix(1,0,0,1,' + Math.round((0 - w) / 2) + ',' + Math.round((th + bh - h) / 2) + ')',
11873									opacity:		1
11874								});
11875								
11876							if (isExternal || isWebPage) {
11877								main.addClass('scrollable');
11878							}
11879							
11880							if (settings.useZoom) {
11881								setZoomBtn(false, false);
11882							}
11883							
11884							// Done Function
11885							if (typeof doneFn === FUNCTION) {
11886								doneFn.call(null);
11887							}
11888							
11889						} else {
11890							// Zoomable types
11891							var imgDims = album.getDimensions(o),
11892								maxDims = album.getMaxDimensions(o) || imgDims,
11893								minRatio,
11894								maxRatio,
11895								targetRatio,
11896								targetDims,
11897								matrix = main.getMatrix(),
11898								optSrc,
11899								isPano,
11900								fitted = false;
11901								
11902							// Stop pano move
11903							if (lightbox.data('panomove')) {
11904								main.trigger('autopanoStop');
11905							}
11906							
11907							// Minimal size	
11908							minRatio = Math.min(1, fitToLightbox(imgDims));
11909							
11910							// Maximum size
11911							if (isVideo || isAudio) {
11912								// Audio or Video: constrain to lightbox
11913								maxRatio = Math.min(maxZoom, fitToLightbox(imgDims));
11914							} else if (maxDims && maxDims[0] > imgDims[0]) {
11915								// Has larger variants
11916								maxRatio = Math.max(1, maxDims[0] / imgDims[0]);
11917							} else {
11918								// Single image
11919								maxRatio = maxZoom;
11920							}
11921							
11922							if (minRatio > maxRatio) {
11923								// Swap if reversed
11924								var r = minRatio;
11925								minRatio = maxRatio;
11926								maxRatio = r;
11927							}
11928							
11929							// Target size (fit image taken into account)
11930							if (settings.useZoom && settings.zoomSlider) {
11931								// Determining initial size for zoom slider
11932								if (main.data('inited')) {
11933									// Already painted
11934									targetRatio = minRatio + zoomLevel * (maxRatio - minRatio) / 100;
11935									targetDims = getTargetDims(imgDims, targetRatio);
11936									fitted = targetRatio == minRatio;
11937									isPano = !!lightbox.data('hasAutoPano');
11938								} else {
11939									// First paint
11940									if (isPanorama || checkPano(imgDims)) {
11941										// Panorama: fit to height
11942										targetRatio = Math.min(maxRatio, fitPano(imgDims));
11943										targetDims = getTargetDims(imgDims, targetRatio);
11944										isPano = checkPanoDims(targetDims);
11945										fitted = true;
11946									} else if (settings.fitImages) {
11947										// Fit to screen
11948										targetRatio = Math.min(maxRatio, fitToLightbox(imgDims));
11949										targetDims = getTargetDims(imgDims, targetRatio);
11950										isPano = false;
11951										fitted = true;
11952									} else {
11953										// Show in 1:1 size
11954										targetRatio = 1;
11955										targetDims = imgDims;
11956										isPano = false;
11957										fitted = targetRatio == minRatio;
11958									}
11959									zoomLevel = 100 * Math.minMax(0, (targetRatio - minRatio) / (maxRatio - minRatio), 1);
11960									setZoomSlider(zoomLevel);
11961								}
11962							} else {
11963								// Fit toggle
11964								if (isPanorama || checkPano(imgDims)) {
11965									targetRatio = Math.min(maxZoom, fitImages? fitPano(imgDims) : fitToLightbox(imgDims));
11966									targetDims = getTargetDims(imgDims, targetRatio);
11967									isPano = checkPanoDims(targetDims);
11968									fitted = fitImages;
11969								} else {
11970									if (!main.data('inited')) {
11971										fitImages = settings.fitImages;
11972										// Initial: fit if larger, 1:1 if smaller
11973										targetRatio = (fitImages || isVideo || isAudio)? Math.min(maxZoom, fitToLightbox(imgDims)) : 1;
11974									} else {
11975										if (fitImages) {
11976											// Zoom out
11977											targetRatio = Math.min(1, fitToLightbox(imgDims));
11978										} else {
11979											// Zoom in
11980											targetRatio = maxRatio;
11981										}
11982									}
11983									fitted = targetRatio == minRatio;
11984									targetDims = getTargetDims(imgDims, targetRatio);
11985								}
11986							}
11987							
11988							// Checking if high res image needed
11989							if (	isImage && !fitted	&&															// It's an image
11990									((maxDims[0] > imgDims[0]) || (maxDims[1] > imgDims[1])) && 					// Has higher resolution version
11991									((targetDims[0] > imgDims[0]) || (targetDims[1] > imgDims[1])) && 				// The target size is larger than the base size
11992									(!main.data('lastDims') || main.data('lastDims')[0] < targetDims[0])			// The higher resolution version hasn't yet been loaded
11993								) {
11994								// Saving optimized image source 
11995								main.data('optSource', album.getOptimalImagePath(o, targetDims, settings.displayOriginals));
11996								main.data('lastDims', targetDims);
11997							}
11998								
11999							// Adding padding (border)
12000							targetDims = [  targetDims[0] + 2 * imgPadding, targetDims[1] + 2 * imgPadding ];
12001																					
12002							// Reset scale
12003							matrix[0] = matrix[3] = 1;
12004							// Setting position
12005							if (!main.data('inited') || (!isPano && fitted)) {
12006								// First paint or fit
12007								main.data('inited', true);
12008								matrix[4] = 0 - targetDims[0] / 2;
12009								matrix[5] = 0 - targetDims[1] / 2  + 
12010										(fitted?
12011											(Math.max(thumbsHeight + controlsHeight, lbPadding) - Math.max(infoHeight, lbPadding)) / 2
12012											:
12013											0
12014										);
12015							} else {
12016								// Already painted.
12017								// Measure the CURRENT on-screen geometry directly, so any pinch-zoom
12018								// scale baked into the transform is taken into account. Using the stale
12019								// matrix[4]/[5] + main.width() (which ignore the pinch scale) is what made
12020								// the focal point drift and the image fly off screen after a pinch.
12021								var lbRect  = lightbox[0].getBoundingClientRect(),
12022									imgRect = main[0].getBoundingClientRect(),
12023									ccx     = lbRect.left + lbRect.width  / 2,	// container centre (viewport)
12024									ccy     = lbRect.top  + lbRect.height / 2,
12025									curLeft = imgRect.left - ccx,				// current border-box edges,
12026									curTop  = imgRect.top  - ccy,				// relative to the container centre
12027									z       = targetDims[0] / imgRect.width;	// visual zoom factor (incl. scale)
12028									
12029								if (typeof center !== UNDEF && center) {
12030									// Zoom toward the event point, keeping it fixed
12031									var	cx = center.x - lbDims[0] / 2,
12032										cy = center.y - lbDims[1] / 2; 
12033									matrix[4] = cx - (cx - curLeft) * z;
12034									matrix[5] = cy - (cy - curTop) * z;
12035								} else {
12036									// Keep the current centre (zoom about the container centre)
12037									matrix[4] = curLeft * z;
12038									matrix[5] = curTop * z;
12039								}
12040							}
12041							
12042							// Prepare panoramas
12043							if (isImage) {
12044								if (isPano) {
12045									// Pamorama
12046									if (!main.data('hasAutoPano')) {
12047										if (fitted && (targetDims[0] > (lbDims[0] + 10) || targetDims[1] > (lbDims[1] + 10))) {
12048											// Applying panorama player
12049											if (targetDims[0] / targetDims[1] > lbDims[0] / lbDims[1]) {
12050												// Horizontal
12051												matrix[4] -= settings.autoPanoStart * (targetDims[0] - lbDims[0]) / 2;
12052											} else {
12053												// Vertical
12054												matrix[5] -= settings.autoPanoStart * (targetDims[1] - lbDims[1]) / 2;
12055											}
12056										}
12057									}
12058								}
12059							}
12060							
12061							// Functions to execute on transition end or immediately if no animation
12062							var fto = null,
12063								finish = function() {
12064										// Replacing image source if not optimal
12065										if (main.data('optSource')) {
12066											var img = main.children('img').eq(0);
12067											if (img.length) {
12068												img.attr('src', main.data('optSource'));
12069												main.removeData('optSource');
12070											}
12071										}
12072										// Adjust or attach pano player
12073										if (isImage && isPano && (typeof center === UNDEF || !center)) {
12074											
12075											if (main.data('hasAutoPano')) {
12076												main.trigger('autopanoAdjust');
12077											} else {
12078												//console.log('Applying autopano');
12079												main.autopano({ 
12080													direction: 	-settings.autoPanoStart || direction,
12081													speed:		settings.autoPanoSpeed
12082												});
12083											}
12084										}
12085									
12086										if (isVideo && settings.videoAuto && media[0].paused) { 
12087											// Play video only after the transition
12088											playVideo(media[0]);
12089										}
12090									},
12091								
12092								endFitImage = function() {
12093										clearTimeout(fto);
12094										// Done Function
12095										if (typeof doneFn === FUNCTION) {
12096											doneFn(finish);
12097										} else {
12098											finish();
12099										}
12100									};
12101						
12102							main.css({
12103									display:		'block',
12104									transition:		animate? ('transform ' + settings.transitionProps + ', width ' + settings.transitionProps + ', height ' + settings.transitionProps) : 'none'
12105								});
12106
12107							// Apply transforms
12108							
12109							if (animate) {
12110								//console.log('Scheduling finish()');
12111								main.one('transitionend', endFitImage);
12112								fto = setTimeout(endFitImage, 320);
12113							}
12114							
12115							main.data({
12116									fitted: 		fitted,
12117									minRatio:		minRatio,
12118									maxRatio:		maxRatio
12119								})
12120								.css({
12121									transform:		'matrix(' + matrix.join(',') + ')',		
12122									width: 			targetDims[0],
12123									height: 		targetDims[1]
12124								});
12125								
12126							// Set zoom button
12127							if (settings.useZoom) {
12128								if (maxRatio > targetRatio) {
12129									setZoomBtn(true, false);
12130								} else if (minRatio < targetRatio) {
12131									setZoomBtn(false, true);
12132								} else {
12133									setZoomBtn(false, false);
12134								}
12135							}
12136							
12137							if (!animate) {
12138								//console.log('Calling finish()');
12139								fto = setTimeout(endFitImage, 20);
12140							}
12141						}
12142						
12143					},
12144				
12145				// Reading image type
12146				
12147				getImageType = function(item) {
12148						
12149						isVideo = isAudio = isOther = isWebPage = isImage = isVr = isPdf = isPanorama = false;
12150						
12151						isExternal = item.hasOwnProperty(J.EXTERNAL);
12152						
12153						isVr = !isExternal && settings.use360Player && (
12154								(item.hasOwnProperty(J.PROJECTIONTYPE) && item[J.PROJECTIONTYPE] === 'equirectangular') 
12155								||
12156								(item[J.CATEGORY] === 'video' && item.hasOwnProperty(J.PANORAMA) && item[J.PANORAMA])
12157							);
12158						
12159						if (!isExternal && !isVr) {
12160							
12161							switch (item[J.CATEGORY]) {
12162								
12163								case 'video':
12164									isVideo = true;
12165									break;
12166								
12167								case 'audio':
12168									isAudio = true;
12169									break;
12170								
12171								case 'other':
12172									isOther = true;
12173									isPdf = album.getExtension(item).toLowerCase() === 'pdf';
12174									break;
12175								
12176								case 'webPage':
12177									isWebPage = true;
12178									break;
12179
12180								default:
12181									isImage = true;
12182									isPanorama =
12183										(item.hasOwnProperty(J.PANORAMA) && item[J.PANORAMA]) 
12184										|| 
12185										(	settings.autoPano 
12186											&& 
12187											(	item[J.IMAGE][J.WIDTH] > lightbox.width()
12188												||
12189												item[J.IMAGE][J.HEIGHT] > lightbox.height()
12190											)
12191											&&
12192											(	(item[J.IMAGE][J.WIDTH] / item[J.IMAGE][J.HEIGHT] > settings.horzPanoThreshold) 
12193												|| 
12194												(item[J.IMAGE][J.WIDTH] / item[J.IMAGE][J.HEIGHT] < settings.vertPanoThreshold)
12195											)
12196										);
12197							}
12198						}
12199					},
12200				
12201				// Gets the custom slideshow interval (if exist)
12202				
12203				getSlideshowTiming = function(item) {
12204					
12205						if (item.hasOwnProperty(J.SLIDESHOWINTERVAL)) {
12206							slideshowDelay = item[J.SLIDESHOWINTERVAL];
12207						} else {
12208							slideshowDelay = settings.slideshowDelay;
12209						}
12210						if (item.hasOwnProperty(J.TRANSITIONSPEED)) {
12211							transitionSpeed = item[J.TRANSITIONSPEED];
12212						} else {
12213							transitionSpeed = settings.transitionSpeed;
12214						}
12215					},
12216					
12217				// Updates rating (if changed externally)
12218					
12219				updateRating = function(r) {
12220						
12221						if (curr >= 0) {
12222							if (typeof r === UNDEF) {
12223								r = thumbs.eq(curr).data(J.OBJ)[J.VISITORRATING] || 0;
12224							}
12225						
12226							lightbox.find('.' + settings.lightboxCaptionClass + ' .ratingbox span')
12227								.removeClass('r0 r1 r2 r3 r4 r5')
12228								.addClass('r' + r);
12229						}
12230					},
12231					
12232				// Paint regions
12233				
12234				paintRegions = function(regions, visible) {
12235						var el = $('<div>', {
12236									'class':	settings.regionsClass
12237								}).appendTo(main);
12238						
12239						for (var i = 0, r; i < regions.length; i++) {
12240							r = regions[i].split(';');
12241							el.append($('<div>').css({
12242									left:		(100 * parseFloat(r[1])) + '%',
12243									top:		(100 * parseFloat(r[2])) + '%',
12244									width:		(100 * parseFloat(r[3])) + '%',
12245									height:		(100 * parseFloat(r[4])) + '%'
12246								}).append('<span>' + r[0] + '</span>'));
12247						}
12248						
12249						el.toggle(typeof visible !== UNDEF && visible);
12250					},
12251					
12252				// Show/hide regions
12253				
12254				toggleRegions = function() {
12255						var el = lightbox.find('.' + settings.regionsClass);
12256					
12257						if (el.length) {
12258							settings.regionsVisible = !settings.regionsVisible;
12259							if (el.is(':visible')) {
12260								el.fadeOut();
12261							} else {
12262								el.fadeIn();
12263							}
12264						}
12265						
12266						return false;
12267					},
12268											
12269				// Creating caption and buttons
12270				
12271				prepareCaption = function(item) {
12272						
12273						caption = $('<div>', {
12274										'class': 	[
12275														settings.lightboxCaptionClass,
12276														'scrollable',
12277														(slideshow || slideshowSuspended)? 'slideshow' : '',
12278														settings.infoPanelAdapt? 'adapt-width' : 'fixed-width'
12279													].filter(Boolean).join(' ')
12280									});
12281						
12282						var txt = $('<div>', {
12283										'class': 	'caption'
12284									}),
12285							buttons = $('<div>', {
12286										'class': 	'buttons'
12287									}),
12288							hasShop,
12289							useLabels = settings.buttonLabels;
12290								
12291						if (item[J.IMAGECAPTION]) {
12292							txt.append(item[J.IMAGECAPTION]);
12293						}
12294						
12295						if (!isExternal && item[J.PHOTODATA] && !settings.metaAsPopup) {
12296							txt.append($('<div>', {
12297										'class':	'photodata',
12298										html:		item[J.PHOTODATA]
12299									}));
12300						}
12301						
12302						if (!txt.is(':empty')) {
12303							txt.find('[data-tooltip]').addTooltip();
12304							caption.append(txt);
12305						} else {
12306							caption.addClass('no-caption');
12307						}
12308						
12309						if (settings.useRating) {
12310							
12311							var r = item[J.RATING] || 0;
12312							
12313							if (settings.visitorRating || r) {
12314								var s;
12315							
12316								el = $('<div>', {
12317										'class':	'ratingbox'
12318									}).appendTo(buttons);
12319									
12320								el.append($('<label>', {
12321										html:		text.rating
12322									}));
12323									
12324								s = $('<span>', {
12325										'class':	'rating',
12326										html:		STARS
12327									})
12328									.appendTo(el);
12329									
12330								if (settings.visitorRating) {
12331									
12332									if (item.hasOwnProperty(J.VISITORRATING)) {
12333										// Already exists
12334										r = item[J.VISITORRATING];
12335									} else {
12336										if (!settings.useJalbumRating) {
12337											// Ignore jAlbum rating
12338											r = 0;
12339										}
12340										// Save the default
12341										item[J.VISITORRATING] = r;
12342									}
12343											
12344									s.addClass('r' + r + (settings.useJalbumRating? '' : ' icon-close'))
12345										.on('click.' + ns, function(e) {
12346												var el = $(this),
12347													r = Math.minMax(0, Math.floor((getCoords(e).x - el.offset().left - parseFloat(el.css('paddingLeft'))) / 16), 5); 
12348													//Math.minMax(0, Math.floor((getCoords(e).x - el.offset().left + 4) / el.width()) * 5, 5);
12349													
12350												el.removeClass('r0 r1 r2 r3 r4 r5').addClass('r' + r);
12351												item[J.VISITORRATING] = r;
12352												
12353												// Propagate to thumbnails
12354												self.trigger('refreshRating', thumbs.eq(curr));
12355													
12356												return false;
12357											});
12358								} else {
12359									// Unalterable rating from jAlbum
12360									s.addClass('r' + r);
12361								}
12362							}
12363						}
12364						
12365						// Extra sizes
12366						
12367						if (!isExternal && extraSizes && item[J.CATEGORY] === 'image') {
12368							var p = album.getImagePath(item);
12369							
12370							el = ($('<div>', {
12371										'class': 	'download icon-download'
12372									})).appendTo(buttons);
12373							
12374							for (var i = 0; i < extraSizes.length; i++) {
12375								el.append($('<a>', {
12376										text: 		extraSizes[i],
12377										href:  		p.replace('slides/', 'dl/' + extraSizes[i] + '/'),
12378										download: 	''
12379									}));
12380							}
12381						}
12382						
12383						// Print button
12384						
12385						if (settings.printImage && item[J.CATEGORY] === 'image' && !settings.rightClickProtect) {
12386							el = $('<a>', {
12387										'class': 	settings.buttonClass + ' icon-printer'
12388									}).appendTo(buttons);
12389							
12390							if (useLabels) {
12391								el.html('<span>' + text.print + '</span>');
12392							}
12393							
12394							if (!ISSEPARATEHOVER) {
12395								el.data('tooltip', useLabels? text.printLabel : text.print);
12396							}
12397							
12398							el.on('click', function(e) {
12399									printImage((item[J.ORIGINAL] && settings.showDownload)? album.getOriginalPath(item) : album.getImagePath(item),
12400										item[J.TITLE] || '',
12401										item[J.THUMBCAPTION] || ''
12402									);
12403								});
12404						}
12405						
12406						// Download button
12407						
12408						if (!isExternal && settings.showDownload) {
12409							var link;
12410							
12411							if (!!item[J.ORIGINAL] && (isImage || isPdf || settings.allowDownloadOthers)) {
12412								// Original of images and PDF's are allowed to download, others with extra permission
12413								link = 	album.getOriginalPath(item);
12414							} else if (isImage && settings.allowDownloadScaled) { 
12415								link = album.getImagePath(item);
12416							}
12417								
12418							if (link) {
12419								
12420								el = $('<a>', {
12421										'class': 		settings.buttonClass + ' icon-download download-btn',
12422										href: 			link,
12423										html:			useLabels? ('<span>' + text.download + '</span>') : '',
12424										download: 		''
12425									}).appendTo(buttons);
12426								
12427								if (!ISSEPARATEHOVER) {
12428									// Adding size as tooltip
12429									getFileSize(link, function(size, el) { 
12430											if (size && el) {
12431												el.addTooltip(useLabels? niceByte(size) : (text.download + ' [' + niceByte(size) + ']'));
12432											}
12433										}, el);
12434								}
12435							}
12436						}
12437						
12438						// PayPal button
12439						
12440						hasShop = settings.hasOwnProperty('shop') && album.hasShop(item) && settings.showShop;
12441						
12442						if (hasShop) {
12443							var txt = album.getPriceRange(item);
12444							
12445							txt = text.addCart + (txt? (' <b>' + txt + '</b>') : '');
12446	
12447							el = $('<a>', {
12448										'class': 		settings.buttonClass + ' icon-shopping-cart-add add-cart-btn'
12449									}).on('click', function(e) {
12450										
12451										if (slideshow) {
12452											suspendAuto();
12453											settings.shop.root.trigger('onClose', function() {
12454													if (slideshowSuspended) {
12455														resumeAuto();
12456													}
12457													settings.shop.root.trigger('onClose', null);
12458												});
12459										}
12460										
12461										settings.shop.root.trigger('addItems', item);
12462										return false;
12463										
12464									}).appendTo(buttons);
12465							
12466							el.html('<span>' + txt + '</span>')
12467								.data('tooltip', text.shopLabel);
12468
12469							el = $('<a>', {
12470										'class': 		settings.buttonClass + ' secondary icon-shopping-cart view-cart-btn'
12471									}).on('click', function(e) {
12472										settings.shop.root.trigger('showCart');
12473										return false;
12474									}).appendTo(buttons);
12475						
12476							el.html('<span>' + text.viewCart + '</span>')
12477								.data('tooltip', text.viewCartLabel);
12478						}
12479						
12480						// Feedback button
12481						
12482						if (settings.hasOwnProperty('feedback') && settings.feedback['root'] && settings.showFeedback) {
12483							el = $('<a>', {
12484										'class': 		settings.buttonClass + ' icon-email-send feedback-btn'
12485									}).on('click', function(e) {
12486									
12487										if (slideshow) {
12488											suspendAuto();
12489											settings.feedback.root.trigger('onClose', function() {
12490													if (slideshowSuspended) {
12491														resumeAuto();
12492													}
12493													settings.feedback.root.trigger('onClose', null);
12494												});
12495										}
12496										
12497										settings.feedback.root.trigger('addItems', item);
12498										return false;
12499										
12500									}).appendTo(buttons);
12501						
12502							if (useLabels) {
12503								el.html('<span>' + settings.feedbackBtnLabel + '</span>')
12504									.data('tooltip', settings.feedbackBtnTooltip);
12505							} else {
12506								el.data('tooltip', settings.feedbackBtnLabel);
12507							}
12508							
12509						}
12510						
12511						// Fotomoto button
12512						
12513						if ((!LOCAL || DEBUG) && settings.hasOwnProperty('fotomoto') && !album.hideFotomoto(item)) {
12514							el = $('<a>', {
12515										'class': 	settings.buttonClass + ' ' + (hasShop? 'icon-fotomoto':'icon-shopping-cart') + ' fotomoto-btn'
12516									}).on('click', function(e) {
12517										e.preventDefault();
12518										
12519										if (slideshow) {
12520											stopAuto(true);
12521										}
12522										
12523										//console.log('Fotomoto button clicked!');
12524										if (typeof FOTOMOTO !== UNDEF) {
12525											FOTOMOTO.API.showWindow(10, 
12526													album.getOriginalPath(item) || album.getItemPath(item)
12527												);
12528										}
12529										return false;
12530									}).appendTo(buttons);
12531						
12532							el.html('<span>' + text.fotomotoBtn + '</span>')
12533								.data('tooltip', text.fotomotoTooltip);
12534						}
12535						
12536						// Photo data
12537						
12538						if (!isExternal && item[J.PHOTODATA] && settings.metaAsPopup) {
12539							el = $('<a>', {
12540										'class': 	settings.buttonClass + ' icon-camera photodata-btn'
12541									}).appendTo(buttons);
12542						
12543							if (useLabels) {
12544								el.html('<span>' + text.metaBtn + '</span>')
12545									.data('tooltip', text.metaLabel);
12546							} else {
12547								el.data('tooltip', text.metaBtn);
12548							}
12549							
12550							el.on('click', function() {
12551									
12552									if (slideshow) {
12553										suspendAuto();
12554									}
12555									
12556									lightbox.modal($('<div>', {
12557											'class':	'photodata',
12558											html: 		item[J.PHOTODATA]
12559										}), {
12560											title: 		text.metaBtn,
12561											icon:		'camera',
12562											onClose:	function() {
12563																if (slideshowSuspended) {
12564																	resumeAuto();
12565																}
12566															}
12567										});
12568									
12569									return false;
12570								});
12571						}
12572						
12573						// Regions
12574						
12575						if (!isExternal && settings.showRegions && item[J.REGIONS]) {
12576							el = $('<a>', {
12577										'class': 	settings.buttonClass + ' icon-facetag regions-btn'
12578									}).appendTo(buttons);
12579						
12580							if (useLabels) {
12581								el.html('<span>' + (settings.regionsBtn || text.regionsBtn) + '</span>')
12582									.data('tooltip', text.regionsLabel);
12583							} else {
12584								el.data('tooltip', (settings.regionsBtn || text.regionsBtn));
12585							}
12586							
12587							paintRegions(JSON.parse(item[J.REGIONS]), settings.regionsVisible); 
12588							
12589							el.on('click', toggleRegions);
12590						}
12591						
12592						// Map
12593						
12594						if (settings.showMap && !!item[J.LOCATION]) {
12595							
12596							el = $('<a>', {
12597										'class': 	settings.buttonClass + ' icon-location map-btn'
12598									}).appendTo(buttons);
12599						
12600							if (useLabels) {
12601								el.html('<span>' + text.mapBtn + '</span>')
12602									.data('tooltip', text.mapLabel);
12603							} else {
12604								el.data('tooltip', text.mapBtn);
12605							}
12606							
12607							el.on('click', function() {
12608									var target = $('<div>', {
12609												'class': 	'map-cont'
12610											});
12611									
12612									if (slideshow) {
12613										suspendAuto();
12614									}
12615									
12616									target.height($window.height() * settings.mapHeight);
12617									
12618									lightbox.modal(target, {
12619											'class': 	'no-padding large',
12620											onClose:	function() {
12621																if (slideshowSuspended) {
12622																	resumeAuto();
12623																}
12624															}
12625										});
12626									
12627									setTimeout(function() {
12628											if (settings.hasOwnProperty('mapApiKey') && settings.mapApiKey) {
12629												// Google
12630												target.addMap({
12631														apiKey:				settings['mapApiKey'],
12632														type:				settings['mapType'] || 'hybrid',
12633														zoom:				settings['mapZoom'] || 16,
12634														location: 			item[J.LOCATION],
12635														fitBounds:			false,
12636														fullscreenControl: 	false,
12637														onTypeChanged:		function(type) {
12638																					settings.mapType = type;
12639																				},
12640														onZoomChanged:		function(zoom) {
12641																					settings.mapZoom = zoom;
12642																				}
12643													});
12644											} else {
12645												// OSM
12646												target.addMap({
12647														resPath:			settings.resPath,
12648														zoom:				settings['mapZoom'] || 16,
12649														location: 			item[J.LOCATION],
12650														fitBounds:			false
12651													});
12652											}
12653										}, 100);
12654									
12655									return false;
12656								});
12657						}
12658						
12659						// Mostphotos
12660						
12661						if (!LOCAL && !isExternal && item[J.MOSTPHOTOS]) {
12662							el = $('<a>', {
12663									'class': 	settings.buttonClass + ' icon-shopping-cart mostphotos-btn',
12664									target: 	'_blank',
12665									href: 		'https://mostphotos.com/' + item[J.MOSTPHOTOS]
12666								}).appendTo(buttons);
12667							
12668							if (useLabels) {
12669								el.html('<span>' + text.mostphotosBtn + '</span>')
12670									.data('tooltip', text.mostphotosLabel);
12671							} else {
12672								el.data('tooltip', text.mostphotosBtn);
12673							}
12674						}
12675						
12676						// Share buttons
12677						
12678						if ((DEBUG || !LOCAL) && settings.share && settings.showShare) {
12679							
12680							el = $('<a>', {
12681									'class':	settings.buttonClass + ' icon-connect share-btn',
12682								}).appendTo(buttons);
12683							
12684							if (useLabels) {
12685								el.html('<span>' + text.share + '</span>')
12686									.data('tooltip', text.shareLabel);
12687							} else {
12688								el.data('tooltip', text.share);
12689							}
12690							
12691							el.on('click', function() {
12692									var target = $('<div>', {
12693												'class': 	'social'
12694											});
12695									
12696									if (slideshow) {
12697										suspendAuto();
12698									}
12699									
12700									lightbox.modal(target, {
12701											title: 		text.shareOn,
12702											icon:		'connect',
12703											onClose:	function() {
12704																if (slideshowSuspended) {
12705																	resumeAuto();
12706																}
12707															}
12708										});
12709									
12710									setTimeout(function() {
12711											target.renderShares({
12712													sites: 			settings.share,
12713													title: 			item[J.TITLE] || item[J.NAME].stripExt().replace(/[-_]/g, ' ').capitalize(),
12714													description: 	item[J.COMMENT],
12715													image: 			album.getOptimalThumbPath(item, [ 960, 640 ], false),
12716													href: 			album.getAbsolutePath(item)
12717												});
12718										}, 100);
12719									
12720									return false;
12721								});
12722						}
12723						
12724						if (settings.showNumbers) {
12725							buttons.prepend($('<div>', {
12726									'class':	'numbers',
12727									'html':		'<span>' + (curr + 1) + '&nbsp;</span>/&nbsp;' + thumbs.length
12728								}));
12729						}
12730						
12731						// Audio clip
12732						
12733						if (!isAudio && item[J.AUDIOCLIP]) {
12734							// Wrapper
12735							el = $('<div>', {
12736									'class':	'audio-clip paused'
12737								}).appendTo(buttons);
12738								
12739							// Button
12740							var btn = $('<a>', {
12741									'class':	settings.buttonClass + ' icon-volume-up audioclip-btn',
12742								}).appendTo(el);
12743							
12744							btn.on('click', function() {
12745									
12746									if (btn.hasClass('icon-pause')) {
12747										pauseAudioClip();
12748									} else {
12749										startAudioClip()
12750									}
12751									
12752									return false;
12753								});
12754							
12755							// Audio element
12756							audioClip = $('<audio>', {
12757										'class':		'audioclip',
12758										src:			album.getAudioClipPath(item),
12759										controlsList:	'nofullscreen nodownload noremote noplaybackrate'
12760									}).appendTo(el);
12761							
12762							// Type attribute saving and duration
12763							if (item[J.AUDIOCLIP].hasOwnProperty(J.TYPE)) {
12764								audioClip[0]['type'] = item[J.AUDIOCLIP][J.TYPE];
12765								if (item[J.AUDIOCLIP].hasOwnProperty(J.DURATION)) {
12766									audioClip.data('duration', item[J.AUDIOCLIP][J.DURATION]);
12767								}
12768							}
12769							
12770							var volume = $('<input class="volume" type="range" name="volume" min="0" max="100" value="' + (settings.audioClipVolume * 100) + '">');
12771							
12772							if (useLabels) {
12773								
12774								btn.append('<span>' + text.play + '</span>')
12775									.data('tooltip', text.audioClipLabel);
12776								
12777								el.append(volume);
12778								
12779							} else {
12780								
12781								btn.data('tooltip', volume);
12782							}
12783							
12784							volume.on('change', function(e) {
12785									var v = $(this).val();
12786									
12787									if (typeof v !== UNDEF) {
12788										settings.audioClipVolume = v / 100;
12789									}
12790									
12791									audioClip[0]['volume'] = settings.audioClipVolume;
12792									savePrefs(settings, settings.prefsKept, 'lb_pref');
12793									
12794									if (typeof settings.onVolumeChange === FUNCTION) {
12795										settings.onVolumeChange(settings.audioClipVolume);
12796									}
12797								});
12798							
12799							// Setting loudness
12800							audioClip[0]['volume'] = settings.audioClipVolume;
12801							
12802							if (settings.autoplayAudioClips) {
12803								// Trying to autoplay
12804								startAudioClip();
12805							}
12806							
12807						} else {
12808							audioClip = $();
12809						}
12810						
12811						if (!buttons.is(':empty')) {
12812							caption.addClass(settings.hasbuttonsClass);
12813							caption.append(buttons);
12814							buttons.find('.button').not('[download]').addTooltip();
12815						}
12816						
12817						if (caption.is(':empty')) {
12818							lightbox.removeClass(settings.captionVisibleClass);
12819							lightbox.addClass('no-caption');
12820						} else {
12821							lightbox.toggleClass(settings.captionVisibleClass, settings.captionVisible);
12822							lightbox.removeClass('no-caption');	
12823							lightbox.append(caption);
12824						}
12825						
12826					},	
12827				
12828				// Preloading the next image (direction sensitive)
12829				
12830				preloadNext = function() {
12831						var nextItem;
12832						
12833						if (direction < 0 && curr > 0) {
12834							nextItem = thumbs.eq(curr - 1).data(J.OBJ);
12835						} else if (curr < thumbs.length - 1) {
12836							nextItem = thumbs.eq(curr + 1).data(J.OBJ);
12837						}
12838						
12839						if (nextItem) {
12840							if (nextItem[J.CATEGORY] === 'image') {
12841								var next = new Image();
12842								next.src = album.getOptimalImagePath(nextItem, getLightboxDims(), settings.displayOriginals);
12843							}
12844						}
12845					},
12846				
12847				// Image clicked :: toggle controls or audio/video controls
12848				
12849				imageClicked = function(e) {
12850						
12851						if ($(e.target).closest('.lightbox-caption').length) {
12852							return true;
12853						}
12854						
12855						var touched = $('html').data('whatinput') === 'touch' || e.type === 'touchend';
12856						
12857						//log(e.type);
12858									
12859						if (isVideo || isAudio) {
12860							var m = media[0];
12861								
12862							// If the tap/click landed on the native media control strip (poster
12863							// play button, scrubber, volume, etc.), let the browser handle it
12864							// natively instead of hijacking play/pause and yanking the controls
12865							// attribute mid-gesture. There's no DOM node to .closest() into here
12866							// since native controls live in the user-agent shadow root, so we
12867							// test the tap's vertical position against the control strip height.
12868							if (m.controls && isTapOnMediaControls(e, m)) {
12869								return true;
12870							}
12871								
12872							if (m.paused) {
12873								m.play();
12874								if (VEND !== 'ms') {
12875									m.controls = false;
12876								}
12877								if (touched) {
12878									toggleControls();
12879									if (lightbox.hasClass(settings.thumbsVisibleClass)) {
12880										hideThumbs();
12881									}
12882									if (lightbox.hasClass(settings.captionVisibleClass)) {
12883										hideCaption();
12884									}
12885								}
12886							} else {
12887								m.pause();
12888								if (VEND !== 'ms') {
12889									m.controls = true;
12890								}
12891								if (touched) {
12892									if (settings.clickAction === 'togglecontrols') {
12893										toggleControls();
12894									} else if (settings.clickAction === 'toggleall') {
12895										if (touched && lightbox.hasClass(settings.controlsHideClass)) {
12896											lightbox.removeClass(settings.controlsHideClass);
12897										} else {
12898											toggleAll();
12899										}
12900									}
12901								}
12902							}
12903							return false;
12904							
12905						} else if (isImage) {
12906							
12907							if (settings.clickAction === 'nextimage') {
12908								// image navigation
12909								if ((getCoords(e).x - main.position().left) > (main.width() / 2)) {
12910									nextImage();
12911								} else {
12912									previousImage();
12913								}
12914								if (touched) {
12915									showControls();
12916								}
12917							} else if (settings.clickAction === 'togglecontrols') {
12918								toggleControls();
12919							} else if (settings.clickAction === 'toggleall') {
12920								if (touched && lightbox.hasClass(settings.controlsHideClass)) {
12921									lightbox.removeClass(settings.controlsHideClass);
12922								} else {
12923									toggleAll();
12924								}
12925							}
12926						}
12927					},
12928				
12929				removeOld = function() {
12930						if (oldMain.length) {
12931							oldMain.remove();
12932							oldMain = $();
12933						}
12934					},
12935					
12936				// Doing all the animations in one timeframe
12937				
12938				doTransitions = function(doneFn) {
12939						
12940						switch (settings.transitionType) {
12941							
12942							case 'crossFadeAndSlide':
12943								var mx = main.getMatrix(),
12944									ox = mx[4];
12945								
12946								if (direction) {
12947									// Offset by 100 pixels
12948									mx[4] += 100 * direction;
12949								
12950									main.css({
12951											transition:		'none',
12952											transform:		'matrix(' +  mx.join(',') + ')'
12953										});
12954								}
12955								
12956								window.requestAnimationFrame(function() {
12957										
12958										if (oldMain.length && oldMain !== main) {
12959											
12960											if (oldMain.data('swipe') === 'swipe') {
12961												setTimeout(removeOld, transitionSpeed / 2);
12962											} else {
12963												var omx = oldMain.getMatrix();
12964												
12965												omx[4] -= 100 * direction;
12966												
12967												oldMain.one('transitionend', removeOld).css({
12968														transition:		'transform ' + (transitionSpeed / 2) + 'ms ease-out, opacity ' + (transitionSpeed / 2) + 'ms ease-out'
12969													}).css({
12970														transform:		'matrix(' + omx.join(',') + ')',
12971														opacity:		0
12972													});
12973											}
12974										}
12975										
12976										main.one('transitionend', function() {
12977												afterTransition(doneFn);
12978											});
12979										
12980										setTimeout(function() {
12981												if (direction) {
12982													// Restore final pos
12983													mx[4] = ox;
12984													// Animate to the final position and opacity
12985													main.css({
12986															transition:		'transform ' + transitionSpeed + 'ms ease-out, opacity ' + transitionSpeed + 'ms ease-out'
12987														}).css({
12988															transform:		'matrix(' + mx.join(',') + ')',
12989															opacity:		1
12990														});
12991												} else {
12992													main.css({
12993															transition:		'transform ' + transitionSpeed + 'ms ease-out, opacity ' + transitionSpeed + 'ms ease-out'
12994														}).css({
12995															opacity:		1
12996														});
12997												}
12998											}, TOUCHENABLED? 350 : 50);
12999									});
13000									
13001								break;
13002								
13003							case 'crossFadeAndZoom':
13004								
13005								var mx = main.getMatrix(),
13006									osx = mx[0],
13007									osy = mx[3];
13008								
13009								if (direction) {
13010									mx[0] *= (1 - direction / 40);
13011									mx[3] *= (1 - direction / 40);
13012									
13013									main.css({
13014											transition:		'none',
13015											transform:		'matrix(' + mx.join(',')  + ')'
13016										});
13017								}
13018								
13019								window.requestAnimationFrame(function() {
13020										
13021										if (oldMain.length && oldMain !== main) {
13022											
13023											if (oldMain.data('swipe') === 'swipe') {
13024												setTimeout(removeOld, transitionSpeed / 2);
13025											} else {
13026												var omx = oldMain.getMatrix();
13027												
13028												omx[0] *= (1 + direction / 20);
13029												omx[3] *= (1 + direction / 20);
13030												
13031												oldMain.one('transitionend', function() {
13032														oldMain.remove();
13033														oldMain = $();
13034													}).css({
13035														transition:		'transform ' + (transitionSpeed / 2) + 'ms ease-out, opacity ' + (transitionSpeed / 2) + 'ms ease-out',
13036														transform:		'matrix(' + omx.join(',') + ')',
13037														opacity:		0
13038													});
13039											}
13040										}
13041										
13042										main.one('transitionend', function() {
13043												afterTransition(doneFn);
13044											});
13045										
13046										setTimeout(function() {
13047												if (direction) {
13048													// Restore final scale
13049													mx[0] = osx;
13050													mx[3] = osy;
13051													// Animate to the final scale and opacity
13052													main.css({
13053															transition:		'transform ' + transitionSpeed + 'ms ease-out, opacity ' + transitionSpeed + 'ms ease-out',
13054															transform:		'matrix(' + mx.join(',') + ')',
13055															opacity:		1
13056														});
13057												} else {
13058													// No direction -> no scale
13059													main.css({
13060															transition:		'transform ' + transitionSpeed + 'ms ease-out, opacity ' + transitionSpeed + 'ms ease-out',
13061															opacity:		1
13062														});
13063												}
13064											}, TOUCHENABLED? 350 : 50);
13065									});
13066								
13067								break;
13068								
13069							default:
13070								
13071								main.css({
13072										transition:		'none',
13073										opacity: 		0
13074									});
13075								
13076								window.requestAnimationFrame(function() {
13077										
13078										if (oldMain.length && oldMain !== main) {
13079											
13080											if (oldMain.data('swipe') === 'swipe') {
13081												setTimeout(removeOld, transitionSpeed / 2);
13082											} else {
13083												oldMain.one('transitionend', function() {
13084														oldMain.remove();
13085														oldMain = $();
13086													}).css({
13087														transition:		'opacity ' + (transitionSpeed / 2) + 'ms ease-out', 
13088														opacity: 		0
13089													});
13090											}
13091										}
13092										
13093										main.one('transitionend', function() {
13094												afterTransition(doneFn);
13095											}).css({
13096												transition: 	'opacity ' + transitionSpeed + 'ms ease-out',
13097												opacity:		1
13098											});
13099										
13100									});
13101						}
13102						
13103					},
13104					
13105				// Transition has completed
13106					
13107				afterTransition = function(doneFn) {
13108						
13109						if (typeof doneFn === FUNCTION) {
13110							doneFn.call();
13111						}
13112						
13113						if (typeof settings.onLoadEnd === FUNCTION) {
13114							settings.onLoadEnd(thumbs.eq(curr));
13115						}
13116						
13117						if (settings.preloadNext) {
13118							preloadNext();
13119						}
13120					},
13121				
13122				// The image is loaded
13123				
13124				imageReady = function() {
13125					
13126						loadCounter--;
13127						
13128						if (loadCounter > 0) {
13129							// Still has to load another image: (poster image) 
13130							return;
13131						}
13132						
13133						lb_activity.hide();
13134						
13135						/*
13136						// Performace metrics
13137						if (typeof performance !== UNDEF) {
13138							
13139							console.log('performance exists');
13140									
13141							var pl = performance.getEntriesByType("resource");
13142							
13143							if (pl && Array.isArray(pl)) {
13144								var p = pl[pl.length - 1];
13145								
13146								console.log('performance last entry: ' + p);
13147								
13148								if (typeof image[0] !== UNDEF && p.name === image[0].src) {
13149									connectionSpeed = connectionSpeed? ((connectionSpeed + p.decodedBodySize / p.duration) / 2) : (p.decodedBodySize / p.duration);
13150								}
13151								
13152								if (DEBUG) {
13153									console.log(p.initiatorType + ':' + p.name.substring(p.name.lastIndexOf('/') + 1) + ' [' + Math.round(p.decodedBodySize / 1000) + 'kB] loaded in ' + Math.round(p.duration) + 'ms => ' + (p.decodedBodySize / p.duration / 1000).toFixed(2) + 'MB/s' + ' (avg:' + (connectionSpeed / 1000).toFixed(2) + 'MB/s)');
13154								}
13155							}
13156							
13157						}
13158						*/
13159						
13160						// Reporting image load in Debug mode
13161						if (DEBUG) {
13162							var d = new Date();
13163							if (isExternal || isVr || isWebPage) {
13164								console.log((isExternal? 'External content' : isWebPage? 'Web page' : '360 player') + ' loaded: ' + (d - loadTimer) + 'ms');
13165							} else if (image.length) {
13166								console.log(((isVideo || isAudio)? 'Media' : 'Image') + ' [' + curr + '] loaded: ' + (d - loadTimer) + 'ms src="' + image[0].src + '"' + (connectionSpeed? (' avg. speed: ' + (connectionSpeed / 1000).toFixed(2) + 'MB/s') : ''));
13167								if (typeof image[0] === UNDEF || typeof media[0] === UNDEF) {
13168									console.log('Premature ' + ((typeof image[0] === UNDEF)? ('loadImage.done(' + image[0].src + ')') : ('loadMedia.done('+ media[0].src + ')')));
13169								}
13170							}
13171						}
13172
13173						// Track the view in Google Analytincs
13174						if (typeof window.gtag === FUNCTION) {
13175							var src = (isVideo || isAudio)? media[0].src : isVr? main.children().eq(0).data('src') : image[0].src,
13176								path = src.getDir().replace(/^(https?\:\/\/[^\/]+\/)/, '');
13177
13178							window.gtag('event', 'image_view', {
13179								image_name: 		src,
13180								album_name: 		path,
13181								image_index: 		curr + 1
13182							});
13183						}
13184
13185						// Right click protect
13186						if ((isImage || isVideo || isAudio) && settings.rightClickProtect) {
13187							media.on('contextmenu', function(e) {
13188									e.preventDefault();
13189									return false;
13190								});
13191						}
13192						
13193						// Audio / video initilazion
13194						if (isVideo || isAudio) {
13195							
13196							media.attr({
13197									autoplay: 	settings.videoAuto,
13198									loop:		settings.videoLoop
13199								});
13200							media[0].volume = settings.volume;
13201							
13202							media.on('volumechange.' + ns, function() {
13203									settings.volume = this.volume;
13204								});
13205						}
13206						
13207						if (oldCaption.length) {
13208							
13209							// Removing the old caption
13210							oldCaption.find('.buttons a').trigger('removeTooltip');
13211							
13212							if (oldCaption.length > 1) {
13213								oldCaption.eq(-1).prevAll('.' + settings.lightboxCaptionClass).remove();
13214								oldCaption = lightbox.find('.' + settings.lightboxCaptionClass);
13215							}
13216							
13217							if (lightbox.hasClass(settings.captionVisibleClass)) {
13218								oldCaption.one('transitionend', function() {
13219										oldCaption.remove();
13220										oldCaption = $();
13221									}).css({
13222										transition:		'opacity ' + (transitionSpeed / 2) + 'ms ease-out',
13223										opacity:		0
13224									});
13225							} else {
13226								oldCaption.remove();
13227							}
13228						}
13229						
13230						if (!isVr && !isPdf && !isWebPage && !isVideo && (!isExternal || !main.children('iframe').length)) {
13231							// Swipe handler
13232							main.swipe({
13233									avoidNavigationOnZoomed:
13234													settings.avoidNavigationOnZoomed,
13235													
13236									onSwipeStart:	function() {
13237															$(this).trigger('autopanoStop');
13238															$(this).hideAllTooltips();
13239														},
13240									
13241									onSwipedLeft: 	function() {
13242															main.data('swipe', 'swipe');
13243															nextImage();
13244														},
13245													
13246									onSwipedRight: 	function() {
13247															main.data('swipe', 'swipe');
13248															previousImage();
13249														},
13250													
13251									onSwipedDown:	function() {
13252															main.data('swipe', 'down');
13253															quitLightbox();
13254														},
13255													
13256									onPinchZoom:	function(e) {
13257															main.data('swipe', 'pinch');
13258															if (lightbox.data('panomove')) {
13259																e.preventDefault();
13260																$(this).trigger('autopanoStop');
13261															}
13262															fixZoom(main.getScale()[0], getCoords(e));
13263														},
13264													
13265									onFinished: 	function() {
13266															if (!dontRemove) {
13267																$(this).trigger('removeSwipe');
13268																$(this).remove();
13269															}
13270														},
13271													
13272									onClick:		function(e) {
13273															if (lightbox.data('panomove')) {
13274																main.data('swipe', 'panostop');
13275																e.preventDefault();
13276																$(this).trigger('autopanoStop');
13277															} else {
13278																main.data('swipe', 'click');
13279																imageClicked(e);
13280															}
13281														},
13282														
13283									onRightClick:	function(e) {
13284															stopAuto(true);
13285														},
13286														
13287									onDoubleClick:	(settings.useZoom && settings.doubleClickZoom)? 
13288														function(e) {
13289																var c = getCoords(e);
13290																main.data('swipe', 'doubleclick');
13291																if (settings.zoomSlider) {
13292																	if (zoomLevel < 100) {
13293																		increaseZoom(50, c);
13294																	} else {
13295																		setZoom(0);
13296																	}
13297																} else {
13298																	toggleZoom(c);
13299																}
13300															} 
13301														: 
13302														null
13303								});
13304							
13305						} /*else if (!isVideo) {
13306
13307							main.on('click.' + ns + ' ' + TOUCH.END + '.' + ns, function(e) {
13308									if (!main.data('clickhandled')) {
13309										main.data('swipe', 'click');
13310										main.data('clickhandled', true);
13311										setTimeout(function() {
13312												// Auto-remove after 200 ms
13313												main.removeData('clickhandled');
13314											}, 200);
13315										imageClicked(e);
13316									}
13317								});
13318							
13319						}*/
13320						
13321						// Placing and centering the new image
13322						fitImage(false, null, function(doneFn) {
13323								// Running transitions
13324								doTransitions(doneFn);
13325							});
13326						
13327						// Check if video has started 
13328						if (isVideo && settings.videoAuto && media[0].paused) {
13329							playVideo(media[0]);
13330						}
13331						
13332						// Continuing slideshow
13333						if (slideshow || slideshowSuspended) {
13334							if ((isAudio || isVideo) && settings.videoAuto) {
13335								suspendAuto();
13336								media[0].onended = function() {
13337									resumeAuto();
13338								};
13339							} else {
13340								clearTimeout(slideshow);
13341								slideshow = setTimeout(nextImage, slideshowDelay);
13342								showProgress();
13343							}
13344						} else {
13345							//stopAuto(true);
13346							if (settings.autohideControls) {
13347								hideControlsLater();
13348							}
13349						}
13350						
13351					},
13352				
13353				// Recentering after swipe for example
13354				
13355				recenter = function(el) {
13356						el = el || main;
13357						if (el.data('swipe') === 'swipe') {
13358							var matrix = main.getMatrix();
13359							matrix[4] = -main.width() / 2;
13360							el.one('transitionend', function() {
13361									el.data('swipe', null);
13362								}).css({
13363									opacity: 		1,
13364									transition:		'opacity ' + (settings.transitionSpeed * 2) + 'ms ease-out, transform ' + (settings.transitionSpeed * 2) + 'ms ease-out',
13365									transform: 		'matrix(' + matrix.join(',') + ')'
13366								});
13367						}
13368					},
13369					
13370				// Showing lightbox
13371				
13372				showLightbox = function() {
13373						
13374						lightbox.css({
13375								display:		'block',
13376								visibility:		'visible',
13377								transition: 	'opacity ' + settings.transitionSpeed + 'ms ease-out'
13378							})
13379							.one('transitionend', function() {
13380									if (settings.autoStart) {
13381										startAuto();
13382									}
13383								})
13384							.css('opacity', 1);
13385							
13386						if (typeof settings.onEnter === FUNCTION) {
13387							settings.onEnter();
13388						}
13389					},
13390						
13391				// Main entry point to load a new image
13392				
13393				loadImage = function(n, doneFn) {
13394						var item;
13395
13396						if (typeof n !== 'number') {
13397							n = thumbs.index(n);
13398						}
13399						
13400						dontRemove = false;
13401
13402						if (n < 0 || n >= thumbs.length) {
13403							// Out of bounds move
13404							
13405							if (n < 0) {
13406								
13407								switch (settings.afterLast) {
13408									case 'donothing':
13409										dontRemove = true;
13410										n = 0;
13411										if (main.data('swipe') === 'swipe') {
13412											recenter();
13413										}
13414										break;
13415										
13416									case 'startover':
13417										n = thumbs.length - 1;
13418										break;
13419										
13420									case 'onelevelup': 
13421										if (settings.level) {
13422											window.location.href = '../' + settings.indexName;
13423										}
13424										n = 0;
13425										break;
13426										
13427									case 'nextfolder':
13428										if (settings.baseSet && settings.previousFoldersLast) {
13429											window.location.href = settings.previousFoldersLast;
13430										} else {
13431											n = 0;
13432											if (main.data('swipe') === 'swipe') {
13433												recenter();
13434											}
13435										}
13436										break;
13437										
13438									case 'nextindex':
13439										if (settings.baseSet && settings.previousFolderPath) {
13440											window.location.href = settings.previousFolderPath;
13441										} else {
13442											n = 0;
13443											if (main.data('swipe') === 'swipe') {
13444												recenter();
13445											}
13446										}
13447										break;
13448										
13449									default:
13450										n = 0;
13451										quitLightbox();
13452										return;
13453								}
13454								
13455							} else {
13456								
13457								switch (settings.afterLast) {
13458									
13459									case 'donothing':
13460										dontRemove = true;
13461										n = thumbs.length - 1;
13462										stopAuto();
13463										if (main.data('swipe') === 'swipe') {
13464											recenter();
13465										}
13466										break;
13467										
13468									case 'startover':
13469										n = 0;
13470										break;
13471										
13472									case 'onelevelup': 
13473										if (settings.level) {
13474											window.location.href = '../' + settings.indexName;
13475										}
13476										n = thumbs.length - 1;
13477										stopAuto();
13478										break;
13479										
13480									case 'nextfolder':
13481										if (settings.baseSet && settings.nextFoldersFirst) {
13482											window.location.href = settings.nextFoldersFirst + (slideshow? '&slideshow' : '');
13483										} else {
13484											n = thumbs.length - 1;
13485											stopAuto();
13486											if (main.data('swipe') === 'swipe') {
13487												recenter();
13488											}
13489										}
13490										break;
13491										
13492									case 'nextindex':
13493										if (settings.baseSet && settings.nextFolderPath) {
13494											window.location.href = settings.nextFolderPath;
13495										} else {
13496											n = thumbs.length - 1;
13497											stopAuto();
13498											if (main.data('swipe') === 'swipe') {
13499												recenter();
13500											}
13501										}
13502										break;
13503										
13504									case 'ask':
13505										
13506										var sr = stopAuto(),
13507											buttons = new Array();
13508										
13509										dontRemove = true;
13510										
13511										if (main.data('swipe') === 'swipe') {
13512											recenter();
13513										}
13514										
13515										n = thumbs.length - 1;
13516							
13517										if (thumbs.length > 1) {
13518											// Start over
13519											buttons.push({
13520													t: 	text.startOver,
13521													c:	'icon-loop',
13522													h: 	function(lb) { 
13523															loadImage(0);
13524														}
13525												});
13526										}
13527										
13528										if (settings.level) {
13529											// Up one level
13530											buttons.push({
13531													t: 	settings.level? text.upOneLevel : (settings.homepageLinkText || text.backToHome), 
13532													c:	'icon-one-level-up',	
13533													h: 	function() { 
13534															window.location.href = '../' + settings.indexName;
13535														}
13536												});
13537										}
13538										
13539										// Back to thumbnails
13540										buttons.push({
13541												t: 	text.backToIndex,
13542												c:	'icon-arrow-up',
13543												h: 	function() { 
13544														quitLightbox();
13545													}
13546											});
13547										
13548										if (settings.baseSet) {
13549											
13550											if (settings.nextFoldersFirst) {
13551												// Go to next folder
13552												buttons.push({
13553														t: 	text.nextFolder,
13554														c:	'icon-arrow-right',
13555														h: 	function() {
13556																window.location.href = settings.nextFoldersFirst + (sr? '&slideshow' : '');
13557															}
13558													});
13559											}
13560								
13561											if (settings.nextFolderPath) {
13562												// Go to next index
13563												buttons.push({
13564														t: 	text.nextIndex,
13565														c:	'icon-thumbnails',
13566														h: 	function() {
13567																window.location.href = settings.nextFolderPath;
13568															}
13569													});
13570											}
13571										}
13572								
13573										lightbox.modal($('<p>', {
13574												'class':	'text-center',
13575												text: 		text.atLastPageQuestion
13576											}), buttons, {
13577												onClose:	function() {
13578																loadImage(thumbs.length - 1);
13579															},
13580												'class': 	'secondary',
13581												icon:		'info',
13582												title: 		text.atLastPage
13583											});
13584										
13585										return;
13586											
13587									default:
13588										// Back to index
13589										n = slideshow? 0 : (thumbs.length - 1);
13590										stopAuto();
13591										// Resetting play position to 0
13592										quitLightbox(thumbs[0]);
13593										return;
13594								}
13595							}
13596						}
13597						
13598						// Direction is calculated if the current thumb exists
13599						if (lightbox.is(':visible')) {
13600							// Lightbox is on
13601							if ((Math.abs(n - curr) >= thumbs.length - 1) && settings.afterLast === 'startover') {
13602								// Flip over
13603								direction = 1;
13604							} else {
13605								direction = (curr > n)? -1 : ((curr < n)? 1 : 0);
13606							}
13607							
13608							if (curr === n) {
13609								// the requested image is already on screen :: nothing to do
13610								return;
13611							}
13612							
13613							main.hideAllTooltips();
13614							
13615							if (settings.autohideControls) {
13616								hideControlsLater();
13617							}
13618							
13619						} else {
13620							// We're on the index page :: show lightbox
13621							listening = true;
13622							$body.add($('html')).css('overflow', 'hidden');
13623							$body.addClass(settings.lightboxOnClass);
13624							direction = 0;
13625							
13626							if (!isFullscreen() && settings.lightboxFullscreen && (SMALLSCREEN === settings.lightboxFullscreenOnlyMobiles)) {
13627								requestFullscreen();
13628							}
13629							
13630							if (settings.useRating) {
13631								updateRating();
13632							}
13633							
13634							showLightbox();
13635							//backgroundAudioOn = backgroundAudioRoot.length && !backgroundAudioRoot.data('paused');
13636
13637							lazyloadThumbs(fitThumbstrip);
13638						}
13639						
13640						closeModal();
13641												
13642						curr = n;
13643						thumb = thumbs.eq(curr);
13644						item = thumb.data(J.OBJ);
13645						
13646						if (settings.useThumbstrip && thumbs.length > 1) {
13647							setActiveThumb();
13648						}
13649						
13650						if (!item) {
13651							console.log('Fatal error: image (' + curr + ') is missing from the database! (Upload data1.json again!)');
13652							return;
13653						}
13654						
13655						lb_activity.show();
13656						hideProgress();
13657						getImageType(item);
13658						getSlideshowTiming(item);
13659						
13660						if (typeof doneFn === FUNCTION) {
13661							doneFn.call(this);
13662						}
13663						
13664						setTimeout(function() {
13665							
13666							oldMain = lightbox.find('.' + settings.lightboxMainClass);
13667							oldCaption = lightbox.find('.' + settings.lightboxCaptionClass);
13668								
13669							if (oldMain.length) {
13670								
13671								oldMain.trigger('autopanoRemove').off('.' + ns).removeClass(settings.currClass);
13672								
13673								if (settings.muteBackgroundAudio && (isAudio || isVideo)) {
13674									muteBackgroundAudio();
13675								}
13676								
13677								fadeMedia(oldMain);
13678								pauseAudioClip();
13679								
13680								oldMain.off('click.nonimage');
13681							}
13682							
13683							// Delayed image loading
13684												
13685							loadCounter = 1;
13686							loadTimer = new Date();
13687							
13688							main = $('<div>', {
13689									'class': 	'lightbox-main ' + settings.currClass + ' ' + (isExternal? 'external' : (isVr? 'vr' : (isPdf? 'pdf' : item[J.CATEGORY])))
13690								})
13691								.css({
13692									opacity: 	0,
13693									transform:	'matrix(1,0,0,1,-50%,-50%)'
13694								})
13695								.appendTo(lightbox);
13696							
13697							if (isWebPage) {
13698								if (item[J.CUSTOMPAGECONTENT]) {
13699									main.append('<div class="page-wrap content">' + item[J.CUSTOMPAGECONTENT].replace(/\${resPath}/g, settings.resPath) + '</div>')
13700										.css({
13701												padding:		'2rem',
13702												width:			Math.min(800, Math.round(lightbox.width() * settings.fitRatio)),
13703												transform: 		'translate(-50%, -50%)'
13704											})
13705										.addClass((album.getOriginalPageName(item) || 'user') + '-page')
13706										.show()
13707										.one('DOMReady', imageReady);
13708								}
13709								// Considered ready after 200ms or the "ready" event, whatever comes first 
13710								setTimeout(imageReady, 200);
13711							} else {
13712									
13713								// Shop and feedback 
13714								if (settings.hasOwnProperty('shop') && album.hasShop(item) ||
13715									settings.hasOwnProperty('feedback') && settings.feedback['root'] && !album.disableFeedback(item)) {
13716									main.append($('<a>', {
13717											'class':		settings.checkboxClass + (thumb.hasClass(settings.selectedClass)? (' ' + settings.checkmarkClass) : '')
13718										}).on('click', function() {
13719											$(this).toggleClass(settings.checkmarkClass);
13720											thumb.trigger('toggleCard');
13721											return false;
13722										}));
13723									if (settings.hasOwnProperty('shop') && album.hasShop(item)) {
13724										main.append($('<a>', {
13725												'class': 		'icon-shopping-cart-add add-cart-btn'
13726											})
13727											.addTooltip(text.addCart, { pos: [0,1,2,1] })
13728											.on('click', function(e) {
13729												if (slideshow) {
13730													suspendAuto();
13731													settings.shop.root.trigger('onClose', function() {
13732															if (slideshowSuspended) {
13733																resumeAuto();
13734															}
13735															settings.shop.root.trigger('onClose', null);
13736														});
13737												}
13738												settings.shop.root.trigger('addItems', item);
13739												return false;
13740											}));
13741									} else {
13742										main.append($('<a>', {
13743												'class': 		'icon-email-send add-cart-btn',
13744												text:			' ' + settings.feedbackAddBtnLabel
13745											}).on('click', function(e) {
13746												if (slideshow) {
13747													suspendAuto();
13748													settings.feedback.root.trigger('onClose', function() {
13749															if (slideshowSuspended) {
13750																resumeAuto();
13751															}
13752															settings.feedback.root.trigger('onClose', null);
13753														});
13754												}
13755												settings.feedback.root.trigger('addItems', item);
13756												return false;
13757											}));
13758									}
13759								}
13760									
13761								if (isVr) {
13762									
13763									main.css({
13764												width:		Math.round(lightbox.width() - 2 * lbPadding),
13765												height:		Math.round(lightbox.height() - 2 * lbPadding)
13766											})
13767										.attr('id', 'vr' + curr)
13768										.addClass('vr')
13769										.show()
13770										.one('DOMReady', imageReady);
13771									
13772									// Photosphere viewer by Jeremy Heleine
13773									if (typeof PhotoSphereViewer !== UNDEF) {
13774										setTimeout(function() {
13775												var opt = { container: 	main[0] },
13776													psv,
13777													gp;
13778													
13779												if (item[J.CATEGORY] === 'video') {
13780													opt.panorama = album.getVideoPath(item);
13781													opt.adapter = PhotoSphereViewer.EquirectangularVideoAdapter;
13782													opt.plugins = [ PhotoSphereViewer.VideoPlugin, {} ];
13783													opt.navbar = [ 'zoom', 'fullscreen' ];
13784												} else {
13785													opt.panorama = album.getOriginalPath(item) || album.getImagePath(item);
13786													opt.navbar = [ 'autorotate', 'zoom', 'fullscreen' ];
13787													if (settings.autoRotate360 && settings.rotateSpeed360) {
13788														opt.autorotateDelay = 1000;
13789													}
13790													opt.autorotateSpeed = settings.rotateSpeed360 + 'rpm';
13791												}
13792												
13793												if (gp = item['gpano']) {
13794													if (gp['fov']) {
13795														opt.defaultZoomLvl = gp.fov;
13796													}
13797													if (gp['pan'] || gp['tilt'] || gp['roll']) {
13798														opt.sphereCorrection = { 
13799																pan:		Math.PI * gp['pan'] / 180, 
13800																tilt:		Math.PI * gp['tilt'] / 180, 
13801																roll:		Math.PI * gp['roll'] / 180
13802															};
13803													}
13804												}
13805													
13806												psv = new PhotoSphereViewer.Viewer(opt);
13807												$(psv.container).data('src', opt.panorama);
13808												// Considered ready after 100ms
13809												setTimeout(imageReady, 100);
13810	
13811											}, 50);
13812									} else {
13813										console.log('Fatal Error: Missing "photo-sphere-viewer.min.js"!');
13814									}
13815									
13816									// Considered ready after 200ms or the "ready" event, whatever comes first 
13817									setTimeout(imageReady, 200);
13818									
13819								} else if (isImage) {
13820									
13821									// Image
13822									// Creating
13823									media = image = $('<img>').appendTo(main);
13824									
13825									if (DEBUG) {
13826										console.log('Loading image [' + curr + '] src="' + album.getOptimalImagePath(item, getLightbox
13826Dims(), settings.displayOriginals) + '"');
13827									}
13828									
13829									if (image[0].complete && image[0].naturalWidth > 0) {
13830										// From cache
13831										imageReady();
13832										
13833									} else {
13834										// Loading
13835										image.one('load.' + ns, function(e) {
13836												// Triggered on load event
13837												imageReady();
13838											});
13839									}
13840									
13841									// Applying the source
13842									image[0].src = album.getOptimalImagePath(item, getLightboxDims(), settings.displayOriginals);
13843									
13844								} else if (isAudio || isVideo) {
13845									
13846									if (isVideo) {
13847										
13848										var	dur = album.getVideoDuration(item);
13849										
13850										// Video
13851										media = image = $('<video>', {
13852													preload: 		'auto',
13853													controlsList:	'nodownload',
13854													poster: 		album.getPosterPath(item)
13855												});
13856										
13857										if (dur === null || dur >= 2000) {
13858											// Normal video
13859											media.attr('controls', 'controls');
13860										} else {
13861											// Short video < 2s
13862											media.attr('loop', 'loop');
13863										}
13864										
13865									} else {
13866										
13867										var src = album.getPosterPath(item);
13868											
13869										// Audio
13870										loadCounter = 2;
13871										
13872										image = $('<img>', {
13873												'class': 	'poster' + (src.endsWith('poster.png')? ' default' : '')
13874											}).one('load', function(e) {
13875												imageReady();
13876											})
13877											.attr('src', src)
13878											.appendTo(main);
13879										
13880										media = $('<audio>', {
13881												preload: 		'auto',
13882												controls: 		'true',
13883												controlsList:	'nofullscreen nodownload'
13884											});
13885									}
13886									
13887									media.one('loadedmetadata', function(e) {
13888												imageReady();
13889											})
13890										.attr('src', album.getSourcePath(item))
13891										.appendTo(main);
13892									
13893									media.on({
13894											playing: 	mediaPlaying,
13895											pause: 		mediaPaused,
13896											ended:		mediaEnded
13897										});
13898											
13899									if (settings.rightClickProtect) {
13900										media.on('contextmenu', function(e) {
13901												e.preventDefault();
13902												return false;
13903											});
13904									}
13905									
13906								} else if (isExternal) {
13907									
13908									var cont = item[J.EXTERNAL]['cont'],
13909										size = item[J.EXTERNAL]['size'];
13910										
13911									if (size) {
13912										size = size.split('x');
13913										main.data({
13914											oWidth:		parseInt(size[0]),
13915											oHeight:	parseInt(size[1])
13916										})
13917									}
13918										
13919									main.one('DOMReady', function(e) {
13920											imageReady();
13921										});
13922									
13923									if (cont.match(/^https?\:\/\//i) || cont.match(/\.html?$/i) || cont.match(/^\.\.\//)) {
13924										main.addClass('iframe')
13925											.append($('<iframe>', { 
13926													width: 				'100%',
13927													height: 			'100%',
13928													src: 				cont,
13929													frameborder: 		0,
13930													allowfullscreen: 	'allowfullscreen'
13931												}));
13932									} else {
13933										if (cont.match(/^<iframe/i)) {
13934											main.addClass('iframe');
13935										}
13936										main.append(cont);
13937									}
13938									
13939									// Considered ready after 200ms or the "ready" event, whatever comes first 
13940									setTimeout(imageReady, 200);
13941									
13942								} else {
13943									// Other
13944									var appendIcon = function(target, item) {
13945															target.append($('<div>', {
13946																	'class':	'icon',
13947																	html:		'<a href="' + album.getSourcePath(item) + '" target="_blank"><img class="other" src="' + album.getImagePath(item) + '"></a><p class="icon-external"> ' + text.clickToOpen + '</p></div>'
13948																}));
13949														};
13950									// Other
13951									if (isPdf && HASPDFVIEWER && !ISIOSDEVICE) {
13952										// using the PDF viewer plugin
13953										var o = $('<object>', {
13954													type:		'application/pdf'
13955												})
13956												.attr('data', album.getSourcePath(item));
13957											
13958										appendIcon(o, item);
13959										main.append(o);
13960										setTimeout(imageReady, 400);
13961										
13962									} else if (settings.useGoogleDocs && !LOCAL && 
13963										'.docx.xlsx.txt.pdf'.indexOf('.' + (item.hasOwnProperty(J.ORIGINAL)? item[J.ORIGINAL][J.PATH] : item[J.NAME]).getExt()) >= 0) {
13964										// Fallback to Google Doc Viewer
13965										main.addClass('iframe');
13966										var target = $('<iframe>', {
13967													width: 				'100%',
13968													height: 			'100%',
13969													frameborder: 		0,
13970													allowfullscreen: 	'allowfullscreen'
13971											}).appendTo(main);
13972										
13973										setTimeout(function(target, item) {
13974												target.attr('src', 'https://docs.google.com/viewer?url=' + window.location.href.getDir() + album.getSourcePath(item) + '&embedded=true');
13975												setTimeout(imageReady, 400);
13976											}, 100, target, item);
13977										
13978									} else {
13979										// Generic case with a download button
13980										appendIcon(main, item);
13981										imageReady();
13982									}
13983								}	
13984								
13985								prepareCaption(item);
13986							}
13987							
13988							if (typeof settings.onLoadStart === FUNCTION) {
13989								settings.onLoadStart(thumb);
13990							}
13991						
13992						}, 30);
13993					},
13994				
13995				// Hiding the lightbox overlay
13996				
13997				quitLightbox = function(thumb) {
13998						
13999						stopAuto(true);
14000						
14001						self.hideAllTooltips();
14002						
14003						listening = false;
14004						$body.add($('html')).css('overflow', '');
14005						$body.removeClass(settings.lightboxOnClass);
14006						$body.hideAllTooltips();
14007						audioClip.remove();
14008						
14009						if (settings.muteBackgroundAudio) {
14010							unmuteBackgroundAudio();
14011						}
14012						
14013						if (main && main.length) {
14014							
14015							if (lightbox.data('panomove')) {
14016								// Panorama playing
14017								main.trigger('autopanoStop');
14018							}
14019							
14020							if ((isAudio || isVideo) && media) {
14021								var sv = media[0].volume,
14022									tm = transitionSpeed / (sv * 50),
14023									fade = function() {
14024											if (media) {
14025												var v = Math.max(media[0].volume - 0.02, 0);
14026												if (v > 0.005) {
14027													media[0].volume = v;
14028													setTimeout(fade, tm);
14029												} else {
14030													media[0].pause();
14031												}
14032											}
14033										};
14034								
14035								media.off('.' + ns);
14036								fade();
14037							}
14038						
14039							window.requestAnimationFrame(function() {
14040									
14041									lightbox.css({
14042											transition: 	'opacity ' + settings.transitionSpeed + 'ms ease-out'
14043										})
14044										.one('transitionend', function() {
14045											// Avoid flicker
14046											lightbox.css('visibility', 'hidden');
14047											setTimeout(function() {
14048													lightbox.css('display', 'none');
14049												}, 20);
14050											
14051											if (isFullscreen() && settings.lightboxFullscreen && (SMALLSCREEN === settings.lightboxFullscreenOnlyMobiles)) {
14052												exitFullscreen();
14053											}
14054											
14055											if (main.length) {
14056												main.remove();
14057												main = $();
14058											}
14059											
14060											if (caption.length) {
14061												caption.remove();
14062												caption = $();
14063											}
14064										})
14065										.css({
14066											opacity:		0
14067										});
14068									});
14069						}
14070						
14071						if (typeof settings.onClose === FUNCTION) {
14072							if (typeof thumb !== UNDEF) {
14073								settings.onClose(thumb);
14074							} else {
14075								settings.onClose();
14076							}
14077						}
14078							
14079						$body.hideAllTooltips();
14080					},
14081				
14082				// Removing the lightbox completely
14083				
14084				removeLightbox = function() {
14085						var remove = function() {
14086									if (isFullscreen() && (
14087											settings.lightboxFullscreen && (SMALLSCREEN === settings.lightboxFullscreenOnlyMobiles)
14088											||
14089											settings.slideshowFullscreen && (SMALLSCREEN === settings.slideshowFullscreenOnlyMobiles)
14090										)) {
14091										exitFullscreen();
14092									}
14093									lightbox.remove();
14094								};
14095						// Stop slideshow		
14096						stopAuto(true);
14097						listening = false;
14098						// removing event handlers
14099						thumbs.off('.' + ns);
14100						$document.add($window).add(lightbox).off('.' + ns);
14101						$body.add($('html')).css('overflow', '');
14102						$body.removeClass(settings.lightboxOnClass);
14103						
14104						if (!main.length) {
14105							// No image; remove immediately
14106							remove();
14107						} else {
14108							// Fade out
14109							lightbox.css('transition', 'opacity ' + settings.transitionSpeed + 'ms ease-out')
14110								.one('transitionend', remove)
14111								.css('opacity', 0);
14112						}
14113					},
14114				
14115				// Set play/pause button
14116				
14117				setPlayPause = function(on) {
14118					
14119						if (lb_play_pause) {
14120							if (on) {
14121								lb_play_pause.removeClass('icon-play').addClass('icon-pause');
14122								lb_play_pause.children('.lbl').text(text.pauseShort);
14123							} else {
14124								lb_play_pause.removeClass('icon-pause').addClass('icon-play').removeClass('show-progress');
14125								lb_play_pause.children('.lbl').text(text.startSlideshowShort);
14126							}
14127						}
14128					},
14129				
14130				// Toggling auto slideshow
14131				
14132				toggleAuto = function() {
14133					
14134						if (slideshow) {
14135							stopAuto();
14136						} else {
14137							startAuto();
14138						}
14139						
14140					},
14141					
14142				// Should the background audio be resumed? 
14143				
14144				isBackgroundAudioOn = function() {
14145						return backgroundAudioRoot.length && !backgroundAudioRoot.data('paused');
14146					},
14147					
14148				// Mute background audio temporarily
14149				
14150				muteBackgroundAudio = function() {
14151						if (isBackgroundAudioOn()) {
14152							backgroundAudioRoot.data('muted', true).trigger('fadeOutPlayer');
14153						}
14154					},
14155					
14156				// Mute background audio temporarily
14157				
14158				unmuteBackgroundAudio = function() {
14159						if (!isBackgroundAudioOn() && backgroundAudioRoot.data('muted')) {
14160							backgroundAudioRoot.data('muted', false).trigger('fadeInPlayer');
14161						}
14162					},
14163					
14164				// Starting auto slideshow
14165				
14166				startAuto = function(keepMusic) {
14167						
14168						clearTimeout(slideshow);
14169						
14170						setPlayPause(true);
14171						
14172						$body.add(lightbox).addClass(settings.immerseClass);
14173						
14174						if (settings.autohideControls) {
14175							hideControlsLater();
14176						}
14177						
14178						caption.addClass('slideshow');
14179						
14180						// Resetting zoom
14181						fitImages = settings.fitImages;
14182						
14183						// Starting background music
14184						if (!(keepMusic === true) && settings.backgroundAudioSlideshowControl) {
14185							backgroundAudioRoot.trigger('fadeInPlayer');
14186						}
14187						
14188						// Full screen mode
14189						if (!isFullscreen() && settings.slideshowFullscreen && (SMALLSCREEN === settings.slideshowFullscreenOnlyMobiles)) {
14190							requestFullscreen();
14191						}
14192						
14193						// Wake lock
14194						requestWakeLock();
14195						
14196						if (audioClipTimeout) {
14197							// Wait until audioclip ends
14198							slideshow = null;
14199							slideshowSuspended = true;
14200							onAudioClipEnded(nextImage);
14201						} else {
14202							// Immediately start
14203							slideshowSuspended = false;
14204							slideshow = setTimeout(nextImage, settings.slideshowDelay / 4);
14205						}
14206						
14207						if (typeof settings.onSlideshowStart === FUNCTION) {
14208							settings.onSlideshowStart(thumb);
14209						}
14210					},
14211									
14212				resumeAuto = function() {
14213						
14214						setPlayPause(true);
14215						
14216						if (settings.autohideControls) {
14217							hideControlsLater();
14218						}
14219						
14220						$body.add(lightbox).addClass(settings.immerseClass);
14221						slideshow = setTimeout(nextImage, settings.slideshowDelay / 4);
14222						slideshowSuspended = false;
14223						
14224						if (settings.backgroundAudioSlideshowControl) {
14225							backgroundAudioRoot.trigger('fadeInPlayer');
14226						}
14227					},
14228		
14229				stopAuto = function(keepMusic) {
14230						var sr = slideshow !== null;
14231						
14232						if (typeof settings.onSlideshowPause === FUNCTION) {
14233							settings.onSlideshowPause(thumb);
14234						}
14235						
14236						if (!(keepMusic === true) && settings.backgroundAudioSlideshowControl) {
14237							backgroundAudioRoot.trigger('fadeOutPlayer');
14238						}
14239						
14240						setPlayPause(false);
14241						
14242						clearTimeout(slideshow);
14243						slideshow = null;
14244						slideshowSuspended = false;
14245						caption.show().removeClass('slideshow');
14246						$body.add(lightbox).removeClass(settings.immerseClass);
14247						
14248						showControls();
14249						
14250						if (isFullscreen() && settings.slideshowFullscreen && (SMALLSCREEN === settings.slideshowFullscreenOnlyMobiles)) {
14251							exitFullscreen();
14252						}
14253						
14254						releaseWakeLock();
14255						
14256						return sr;
14257					},
14258				
14259				suspendAuto = function() {
14260					
14261						if (settings.backgroundAudioSlideshowControl) {
14262							muteBackgroundAudio();
14263						}
14264						
14265						clearTimeout(slideshow);
14266						slideshow = null;
14267						slideshowSuspended = true;
14268					},
14269					
14270				updateAudioClipProgress = function(pb) {
14271					
14272						if (audioClip.length && pb.length) {
14273							pb.css('width', (100 * audioClip[0].currentTime / audioClip[0].duration) + '%');
14274						}
14275					},
14276					
14277				startAudioClip = function() {
14278					
14279						if (audioClip.length) {
14280							
14281							audioClip[0].play().then(function() {
14282									var btn = audioClip.siblings('.audioclip-btn'),
14283										pb = btn.children('.progress');
14284										
14285									btn.removeClass('icon-volume-up').addClass('icon-pause');
14286									audioClip.parent().removeClass('paused').addClass('playing');
14287									
14288									if (slideshow) {
14289										suspendAuto();
14290									}
14291									
14292									if (!pb.length) {
14293										pb = $('<div>', {
14294												'class':	'progress'
14295											}).appendTo(btn);
14296									}
14297									
14298									updateAudioClipProgress(pb);
14299									
14300									audioClipTimeout = setInterval(updateAudioClipProgress, 100, pb);
14301									
14302									audioClip.on('ended', function() {
14303										
14304											btn.removeClass('icon-pause').addClass('icon-volume-up');
14305											audioClip.parent().removeClass('playing').addClass('paused');
14306											
14307											if (slideshowSuspended) {
14308												resumeAuto();
14309											}
14310											
14311											pb.remove();
14312											
14313											clearInterval(audioClipTimeout);
14314										});
14315											
14316								}, function(err) {
14317									
14318									btn.removeClass('icon-pause').addClass('icon-volume-up');
14319									console.log(err);
14320								});
14321						}
14322					},
14323					
14324				pauseAudioClip = function() {
14325							
14326						if (audioClip.length) {
14327							var btn = audioClip.siblings('.audioclip-btn');
14328							
14329							audioClip[0].pause();
14330							clearInterval(audioClipTimeout);
14331							audioClipTimeout = null;
14332
14333							btn.removeClass('icon-pause').addClass('icon-volume-up');
14334							audioClip.parent().removeClass('playing').addClass('paused');
14335							
14336							if (slideshowSuspended) {
14337								resumeAuto();
14338							}
14339						}
14340					},	
14341					
14342				onAudioClipEnded = function(fn, params) {
14343					
14344						if (audioClip.length && typeof fn === FUNCTION) {
14345							if (audioClip[0].paused) {
14346								if (typeof params !== UNDEF) {
14347									fn.apply(null, params);
14348								} else {
14349									fn.call(null);
14350								}
14351							} else {
14352								audioClip.on('ended', fn, params);
14353							}
14354						}
14355					},
14356					
14357				mediaPlaying = function() {
14358					
14359						if (settings.muteBackgroundAudio) {
14360							muteBackgroundAudio();
14361						}
14362						startAudioClip();
14363						main.addClass(settings.playingClass);
14364					},
14365					
14366				mediaPaused = function() {
14367						
14368						if (settings.muteBackgroundAudio) {
14369							// Unmute after 500ms so it won't start between videos in slideshows
14370							setTimeout(unmuteBackgroundAudio, 500);
14371						}
14372						pauseAudioClip();
14373						main.removeClass(settings.playingClass);
14374					},
14375					
14376				mediaEnded = function() {
14377					
14378						pauseAudioClip();
14379						main.removeClass(settings.playingClass);
14380					},
14381				
14382				fadeMedia = function(el) {
14383						var media = (el || lightbox).find('audio,video').eq(0);
14384						
14385						if (media.length) {
14386							var sv = media[0].volume,
14387								tm = settings.transitionSpeed / (sv * 50),
14388								fade = function() {
14389										if (media) {
14390											var v = Math.max(media[0].volume - 0.02, 0);
14391											if (v > 0.005) {
14392												media[0].volume = v;
14393												setTimeout(fade, tm);
14394											} else {
14395												media[0].pause();
14396											}
14397										}
14398									};
14399							
14400							media.off('volumechange.' + ns);
14401							fade();
14402						}
14403					},
14404					
14405				stopMedia = function(el) {
14406						var m = (el || lightbox).find('audio,video').eq(0);
14407						
14408						if (m.length) {
14409							m[0].pause();
14410						}
14411					},
14412					
14413				showControls = function() {
14414						clearTimeout(controlsTimeout);
14415						lightbox.removeClass(settings.controlsHideClass);
14416					},
14417				
14418				hideControls = function() {
14419						clearTimeout(controlsTimeout);
14420						controls.hideAllTooltips();
14421						lightbox.addClass(settings.controlsHideClass);
14422					},
14423					
14424				hideControlsLater = function() {
14425						clearTimeout(controlsTimeout);
14426						controlsTimeout = setTimeout(function() {
14427								//$body.hideAllTooltips();
14428								hideControls();
14429							}, /*slideshow? Math.min(settings.slideshowDelay / 2, settings.hideControlsDelay) :*/ settings.hideControlsDelay);
14430					},
14431	
14432				toggleControls = function() {
14433						clearTimeout(controlsTimeout);
14434						lightbox.toggleClass(settings.controlsHideClass, !controlsHidden());
14435					},
14436				
14437				controlsHidden = function() {
14438						return lightbox.hasClass(settings.controlsHideClass);
14439					},
14440				
14441				toggleFullscreen = function() {
14442						if (isFullscreen()) {
14443							exitFullscreen();
14444						} else {
14445							requestFullscreen();
14446						}
14447					},
14448					
14449				showThumbs = function() {
14450						controls.hideAllTooltips();
14451						lightbox.addClass(settings.thumbsVisibleClass);
14452						settings.thumbsVisible = thumbsVisibleDefault = true;
14453						savePrefs(settings, settings.prefsKept, 'lb_pref');
14454						if (main.data('fitted')) {
14455							// re-adjusting position
14456							fitImage(true);
14457						}
14458					},
14459				
14460				hideThumbs = function() {
14461						controls.hideAllTooltips();
14462						lightbox.removeClass(settings.thumbsVisibleClass);
14463						settings.thumbsVisible = thumbsVisibleDefault = false;
14464						savePrefs(settings, settings.prefsKept, 'lb_pref');
14465						if (main.data('fitted')) {
14466							// re-adjusting position
14467							fitImage(true);
14468						}
14469					},
14470				
14471				showCaption = function() {
14472						lightbox.addClass(settings.captionVisibleClass);
14473						settings.captionVisible = captionVisibleDefault = true;
14474						savePrefs(settings, settings.prefsKept, 'lb_pref');
14475						if (main.data('fitted')) {
14476							// re-adjusting position
14477							fitImage(true);
14478						}
14479					},
14480					
14481				hideCaption = function() {
14482						controls.hideAllTooltips();
14483						lightbox.removeClass(settings.captionVisibleClass);
14484						settings.captionVisible = captionVisibleDefault = false;
14485						savePrefs(settings, settings.prefsKept, 'lb_pref');
14486						if (main.data('fitted')) {
14487							// re-adjusting position
14488							fitImage(true);
14489						}
14490					},
14491				
14492				toggleCaption = function() {
14493						if (settings.captionVisible) {
14494							hideCaption();
14495						} else {
14496							showCaption();
14497						}
14498					},
14499						
14500				toggleAll = function() {
14501						if (settings.captionVisible || settings.thumbsVisible) {
14502							thumbsVisibleDefault = settings.thumbsVisible;
14503							captionVisibleDefault = settings.captionVisible;
14504							// Hide everything
14505							controls.hideAllTooltips();
14506							clearTimeout(controlsTimeout);
14507							lightbox.addClass(settings.controlsHideClass);
14508							lightbox.removeClass(settings.thumbsVisibleClass + ' ' + settings.captionVisibleClass);
14509							settings.thumbsVisible = settings.captionVisible = false;
14510						} else {
14511							showControls();
14512							// Show items only to be visible by default
14513							if ((thumbsVisibleDefault || !captionVisibleDefault) && !settings.thumbsVisible) {
14514								lightbox.addClass(settings.thumbsVisibleClass);
14515								settings.thumbsVisible = true;
14516							}
14517							if ((captionVisibleDefault || !thumbsVisibleDefault) && !settings.captionVisible) {
14518								lightbox.addClass(settings.captionVisibleClass);
14519								settings.captionVisible = true;
14520							}
14521						}
14522						
14523						savePrefs(settings, settings.prefsKept, 'lb_pref');
14524						if (settings.fitBetween !== 'screen') {
14525							if (main.data('fitted')) {
14526								// re-adjusting position
14527								fitImage(true);
14528							}
14529						}
14530					},
14531					
14532				previousImage = function() {
14533						loadImage(curr - 1);
14534					},
14535				
14536				nextImage = function() {
14537						loadImage(curr + 1);
14538					},
14539				
14540				moveThumbs = function(x) {
14541						
14542						lb_thumbs
14543							.css({
14544								transition:		'transform ' + settings.thumbSpeed + 'ms ease-out',
14545								transform:		'translateX(' + x + 'px)'
14546							});
14547							
14548						lazyloadThumbs();
14549					},
14550				
14551				setActiveThumb = function() {
14552					
14553						if (!settings.useThumbstrip || thumbs.length < 2) {
14554							return;
14555						}
14556						
14557						var cthumb = lb_thumbs.children().eq((curr < 0)? 0 : curr),
14558							tl = cthumb.position().left,
14559							tw = cthumb.width(),
14560							cl = lb_thumbs.getTranslate()[0],
14561							cw = lb_thumb_cont.width(),
14562							tsw = lb_thumbs.width();
14563							
14564						lb_thumbs.children().removeClass(settings.activeClass);
14565						cthumb.addClass(settings.activeClass);
14566						
14567						if (tl < -cl) {
14568							moveThumbs(Math.min(0, settings.thumbOverhead - tl));
14569						} else  if ((tl + tw) > (cw - cl)) {
14570							moveThumbs(Math.max(cw - tl - tw - settings.thumbOverhead, cw - tsw));
14571						} else if ((cw - cl) > tsw) {
14572							moveThumbs(cw - tsw);
14573						}
14574							
14575					},
14576				
14577				// Adding one thumb
14578				
14579				loadThumb = function(thumb) {
14580						
14581						if (thumb.length) {
14582							var i = $('<img>', {
14583									'class': 	'hide-image'
14584								})
14585								// Onload action
14586								.one('load', function() {
14587									$(this).removeClass('hide-image').addClass('show-image');
14588									$(this).parent().removeClass(settings.lazyloadClass);
14589								})
14590								.attr('src', thumb.attr('href'));
14591							
14592							if (settings.rightClickProtect) {
14593								i.on('contextmenu', function(e) {
14594										e.preventDefault();
14595										return false;
14596									});
14597							}
14598							
14599							thumb.append(i).removeAttr('href');
14600						}
14601											
14602					},
14603					
14604				// Loading visible thumbs
14605				
14606				lazyloadThumbs = function(callback) {
14607					
14608						if (!lightbox.is(':visible') || !settings.useThumbstrip || thumbs.length < 2) {
14609							return;
14610						}
14611						
14612						var cw = lb_thumb_cont.width(),
14613							cl = lb_thumbs.getTranslate()[0];
14614						
14615						lb_thumbs.children('.' + settings.lazyloadClass).each(function() {
14616							var t = $(this),
14617								tl = t.position().left,
14618								tw = t.width();
14619								
14620							if (((tl + cl) < (cw * 7 / 4)) && ((tl + tw + cl) > (0 - cw * 3 / 4))) {
14621								// In view
14622								t.removeClass(settings.lazyloadClass);
14623								loadThumb(t);
14624							} else if ((tl + cl) >= cw) {
14625								// Right to clip window
14626								return false;
14627							}
14628						});
14629						
14630						if (typeof callback === FUNCTION) {
14631							callback.call(this);
14632						}
14633					},
14634					
14635				// Fit thumbnail strip if shorter than the original space
14636				
14637				fitThumbstrip = function() {
14638						if (!lightbox.is(':visible')) {
14639							return;
14640						}
14641						
14642						var lt = lb_thumbs.children(':last-child');
14643	
14644						if ((lt.position().left + lt.outerWidth()) > lb_thumb_cont.width()) {
14645							lb_thumbstrip.addClass(settings.scrollClass);
14646							setActiveThumb();
14647						} else {
14648							lb_thumbstrip.removeClass(settings.scrollClass);
14649							moveThumbs(0);
14650						}
14651					},
14652					
14653				// Scrolling the thumb strip
14654				
14655				scrollThumbs = function(direction) {
14656						var cw = lb_thumb_cont.width(),
14657							cl = lb_thumbs.getTranslate()[0];
14658					
14659						if (cw > lb_thumbs.width()) {
14660							if (cl) {
14661								moveThumbs(0);
14662							}
14663						} else {
14664							cl = (direction < 0)? 
14665								Math.min(0, cl + cw)
14666								:
14667								Math.max(cw - lb_thumbs.width(), cl - cw);
14668							moveThumbs(cl);
14669						}
14670					},
14671									
14672				// Settings up the thumb strip
14673				
14674				initThumbstrip = function() {
14675					
14676						if (settings.thumbsVisible) {
14677							lightbox.addClass(settings.thumbsVisibleClass);
14678						}
14679						
14680						// Thumbsstrip
14681						lb_thumbstrip = $('<div>', {
14682								'class':	'thumb-strip'
14683							})
14684							.appendTo(lightbox);
14685							
14686						lb_thumbstrip.append($('<button>', {
14687								'class':	'left icon-caret-left'
14688							}).on('click.' + ns, function(e) {
14689								scrollThumbs(-1);
14690								return false;
14691							}));
14692							
14693						lb_thumbstrip.append($('<button>', {
14694								'class':	'right icon-caret-right'
14695							}).on('click.' + ns, function(e) {
14696								scrollThumbs(1);
14697								return false;
14698							}));
14699							
14700						lb_thumb_cont = $('<div>', {
14701								'class':	'thumb-cont'
14702							}).appendTo(lb_thumbstrip);
14703							
14704						lb_thumb_cont.on('selectstart.' + ns, function(e) {
14705								return false;
14706							});
14707							
14708						lb_thumbs = $('<div>', {
14709								'class':	'thumbs'
14710							}).appendTo(lb_thumb_cont);
14711							
14712						var item,
14713							w,
14714							h = lb_thumbs.height() || 60,
14715							a,
14716							tw = 0;
14717							
14718						thumbs.each(function(i) {
14719								
14720							item = $(this).data(J.OBJ);
14721							
14722							a = $('<a>', {
14723									'class':	settings.lazyloadClass,
14724									title:		item[J.THUMBCAPTION] || '',
14725									href:		album.getThumbPath(item)
14726								}).appendTo(lb_thumbs);
14727							
14728							a.on('click', function(e) {
14729									var a = $(e.target).closest('a');
14730									
14731									loadImage(lb_thumbs.children().index(a));
14732									return false;
14733								});
14734							
14735							w = Math.floor(Math.max((h - 4) * 3 / 4, Math.min(item[J.THUMB][J.WIDTH], (h - 4) * item[J.THUMB][J.WIDTH] / item[J.THUMB][J.HEIGHT])));
14736							
14737							a.width(w);
14738							
14739							tw += w;
14740						});
14741					},
14742					
14743				// Setting up the controls
14744				
14745				initControls = function() {
14746						// Control strip
14747						controls = $('<div>', {
14748									'class':	settings.controlsClass + (settings.controlsUseText? ' use-text' : '')
14749								}).appendTo(lightbox);
14750						
14751						// Left button
14752						if (thumbs.length > 1) { 
14753							lb_btn_left = $('<button>', {
14754									type: 		'button',
14755									'class': 	'previous',
14756									title:		text.previousPicture,
14757									text:		settings.controlsUseText? text.previousPictureShort : ''
14758								})
14759								.on('click.' + ns, previousImage)
14760								.appendTo(controls);
14761						}
14762							
14763						// Up button
14764						lb_up = $('<button>', {
14765								type: 		'button',
14766								'class': 	'up icon-arrow-up',
14767								title:		text.upOneLevel,
14768								text:		settings.controlsUseText? text.upOneLevelShort : ''
14769							})
14770							.on('click.' + ns, function() { quitLightbox(); })
14771							.appendTo(controls);
14772						
14773						// Zoom button
14774						if (settings.useZoom) {
14775							
14776							// Zoom button(s)
14777							if (settings.zoomSlider) {
14778								// Slider
14779								var zoom_panel= $('<div>', {
14780											'class':	'zoom-control'
14781										}),
14782									zoom_in_btn,
14783									zoom_out_btn;
14784								
14785								zoom_out_btn = $('<a>', {
14786										'class':	'zoom-out button'
14787									})
14788									.on('click', function() {
14789										decreaseZoom();
14790										return false;
14791									})
14792									.appendTo(zoom_panel);
14793										
14794								lb_zoom_slider = $('<input>', {
14795										name:		'zoom-level',
14796										type:		'range'
14797									})
14798									.on('change', function() {
14799										setZoom(parseInt(lb_zoom_slider.val()));
14800										return true;
14801									})
14802									.appendTo(zoom_panel);
14803										
14804								zoom_in_btn = $('<a>', {
14805										'class':	'zoom-in button'
14806									})
14807									.on('click', function() {
14808										increaseZoom();
14809										return false;
14810									})
14811									.appendTo(zoom_panel);
14812									
14813								// Zoom toggle button
14814								lb_zoom = $('<button>', {
14815										type: 		'button',
14816										'class': 	'zoom-level icon-zoom-level',
14817										text:		settings.controlsUseText? text.zoom : ''
14818									})
14819									.on('click.' + ns, function(){
14820										if (listening) {
14821											setTimeout(toggleZoom, 50);
14822											if ($('html').data('whatinput') === 'touch') {
14823												// Touch toggle
14824												return true;
14825											}
14826										}
14827										return false; 
14828									})
14829									.addTooltip($('<div>', {
14830										'class':	'zoom-panel'
14831									})
14832										.append(zoom_panel), {
14833										touchToggle:	true,
14834										pos:			[1,0,1,2]
14835									})
14836									.appendTo(controls);
14837									
14838							} else {
14839								
14840								// Zoom toggle button
14841								lb_zoomin = $('<button>', {
14842										type: 		'button',
14843										'class': 	'zoom-in icon-expand',
14844										title: 		text.oneToOneSize,
14845										text:		settings.controlsUseText? text.oneToOneSizeShort : ''
14846									})
14847									.on('click.' + ns, function() { 
14848										toggleZoom(); 
14849										return false; 
14850									})
14851									.appendTo(controls);
14852								
14853								lb_zoomout = $('<button>', {
14854										type: 		'button',
14855										'class': 	'zoom-out icon-contract',
14856										title: 		text.fitToScreen,
14857										text:		settings.controlsUseText? text.fitToScreenShort : ''
14858									})
14859									.on('click.' + ns, function() { 
14860										toggleZoom(); 
14861										return false; 
14862									})
14863									.appendTo(controls);
14864									
14865								lb_zoom_slider = $();
14866							}
14867						}
14868						
14869						// Hide/show top thumbs
14870						if (settings.useThumbstrip && thumbs.length > 1) {
14871							lb_showthumbs = $('<button>', {
14872									type: 		'button',
14873									'class': 	'show-thumbs icon-show-top-panel',
14874									title: 		text.showThumbs,
14875									text:		settings.controlsUseText? text.showThumbsShort : ''
14876								})
14877								.on('click.' + ns, showThumbs)
14878								.appendTo(controls);
14879						
14880							lb_hidethumbs = $('<button>', {
14881									type: 		'button',
14882									'class': 	'hide-thumbs icon-hide-top-panel',
14883									title: 		text.hideThumbs,
14884									text:		settings.controlsUseText? text.hideThumbsShort : ''
14885								})
14886								.on('click.' + ns, hideThumbs)
14887								.appendTo(controls);
14888						}
14889							
14890						// Hide/show captions
14891						lb_showcaption = $('<button>', {
14892								type: 		'button',
14893								'class': 	'show-caption icon-show-bottom-panel',
14894								title: 		text.showInfo,
14895								text:		settings.controlsUseText? text.showInfoShort : ''
14896							})
14897							.on('click.' + ns, showCaption)
14898							.appendTo(controls);
14899						
14900						lb_hidecaption = $('<button>', {
14901								type: 		'button',
14902								'class': 	'hide-caption icon-hide-bottom-panel',
14903								title: 		text.hideInfo,
14904								text:		settings.controlsUseText? text.hideInfoShort : ''
14905							})
14906							.on('click.' + ns, hideCaption)
14907							.appendTo(controls);
14908							
14909						if (settings.useSlideshow && thumbs.length > 1) {
14910							
14911							// Play/Pause button
14912							lb_play_pause = $('<button>', {
14913									type: 		'button',
14914									'class': 	'play-pause icon-play',
14915									'title': 	text.slideshow
14916								})
14917								.appendTo(controls);
14918							
14919							if (settings.controlsUseText) {
14920								lb_play_pause.append($('<span>', {
14921										'class':	'lbl',
14922										html:		text.startSlideshowShort
14923									}));
14924							}
14925							
14926							lb_progress = $('<span>', {
14927									'class': 	'progress'
14928								})
14929								.appendTo(lb_play_pause);
14930								
14931							lb_play_pause.on('click.' + ns, function(){ 
14932									if (listening) {
14933										toggleAuto();
14934										if ($('html').data('whatinput') === 'touch') {
14935											// Touch toggle
14936											return true;
14937										}
14938									}
14939									return false; 
14940								});
14941									
14942							if (settings.timingControl) {
14943								// Slideshow timing control
14944								var timing_panel= $('<div>', {
14945											id:			'timing-control',
14946											'class':	'timing-control'
14947										}),
14948									decr_time_btn = $('<a>', {
14949											'class':	'decr-time'
14950										}).appendTo(timing_panel);
14951										
14952								lb_timing_value = $('<span>').appendTo(timing_panel);
14953										
14954								var incr_time_btn = $('<a>', {
14955											'class':	'incr-time'
14956										}).appendTo(timing_panel);
14957									
14958								lb_play_pause.addTooltip($('<div>', {
14959											'class':	'timing-panel'
14960										}).append($('<div>', {
14961											'class':	'label',
14962											html:		text.timing
14963										})).append(timing_panel), {
14964											touchToggle:	true,
14965											delay:			600,
14966											pos:			[1,0,1,2]
14967										});
14968									
14969								incr_time_btn.on('click', function() {
14970										increaseTiming();
14971										return false;
14972									});
14973								
14974								decr_time_btn.on('click', function() {
14975										decreaseTiming();
14976										return false;
14977									});
14978								
14979								setTimeout(function() { 
14980										setTiming(settings.slideshowDelay);
14981									}, 500);
14982								
14983							} else {
14984								// No timing control
14985								lb_play_pause.attr('title', text.startStopSlideshow);
14986							}
14987						}
14988								
14989						// Full screen toggle button
14990						if (settings.showFullscreen && hasFullscreen()) {
14991							lb_fullscr = $('<button>', {
14992									type: 		'button',
14993									'class': 	'gofullscreen icon-fullscreen',
14994									title: 		text.fullscreen,
14995									text:		settings.controlsUseText? text.fullscreenShort : ''
14996								})
14997								.on('click.' + ns, requestFullscreen)
14998								.appendTo(controls);
14999							
15000							lb_exitfullscr = $('<button>', {
15001									type: 		'button',
15002									'class': 	'exitfullscreen icon-fullscreen-off',
15003									title: 		text.exitFullscreen,
15004									text:		settings.controlsUseText? text.exitFullscreenShort : ''
15005								})
15006								.on('click.' + ns, exitFullscreen)
15007								.appendTo(controls);
15008								
15009							if (isFullscreen()) {
15010								lightbox.addClass('fullscreen');
15011							}
15012						}
15013							
15014						// Right button
15015						if (thumbs.length > 1) { 
15016							lb_btn_right = $('<button>', {
15017									type: 		'button',
15018									'class': 	'next',
15019									title:		text.nextPicture,
15020									text:		settings.controlsUseText? text.nextPictureShort : ''
15021								})
15022								.on('click.' + ns, nextImage)
15023								.appendTo(controls);
15024						}
15025						
15026						// Adding tooltips
15027						controls.children('button').not('.zoom-level').not('.pause').addTooltip({
15028								delay:		1000,
15029								pos:		[1,0,1,2]
15030							});
15031			
15032					},
15033					
15034				// Setting up the structure
15035				
15036				initLightbox = function() {
15037					
15038						lightbox = $('<div>', {
15039								id: 		ns,
15040								'class': 	'lightbox'
15041							})
15042							.hide()
15043							.appendTo(self);
15044					
15045						// Darken background
15046						lb_overlay = $('<div>', {
15047								'class': 	'lightbox-overlay'
15048							})
15049							.appendTo(lightbox);
15050					
15051						// Activity indicator
15052						lb_activity = $('<div>', {
15053								'class': 	'lightbox-loading'
15054							})
15055							.append('<div>')
15056							.append('<div>')
15057							.append('<div>')
15058							.appendTo(lightbox);
15059						
15060						// Apply visibility classes
15061						lightbox.toggleClass(settings.thumbsVisibleClass, settings.thumbsVisible && thumbs.length > 1);
15062						lightbox.toggleClass(settings.captionVisibleClass, settings.captionVisible);
15063						//lightbox.toggleClass('zoomed', zoomed);
15064						
15065						// Controls
15066						initControls();
15067						
15068						// Thumb strip
15069						if (settings.useThumbstrip && thumbs.length > 1) {
15070							initThumbstrip();
15071						}
15072						
15073						// Slideshow delay and transition speed
15074						if (!settings.hasOwnProperty('slideshowDelay')) {
15075							slideshowDelay = settings.slideshowDelay = 4000;
15076						}
15077						if (!settings.hasOwnProperty('transitionSpeed')) {
15078							transitionSpeed = settings.transitionSpeed = 400;
15079						}
15080						
15081						// Quit beside handler
15082						if (!settings.quitOnDocClick) {
15083							lb_overlay.on('click.' + ns, function(e) {
15084									
15085									if (listening) {
15086										if (this === e.target) {
15087											var p = getCoords(e);
15088											
15089											if (p.x >= lightbox.width() / 2) {
15090												nextImage();
15091											} else {
15092												previousImage();
15093											}
15094										}
15095										return false;
15096									}
15097								});
15098						}
15099					};
15100					
15101					
15102			// Initializing Lightbox
15103						
15104			if (ns) {
15105				// should never happen, but still living lightbox attached to thumb elements
15106				cleanup(ns);
15107			}
15108			
15109			// Creating new namespace
15110			self.data('llb_ns', ns = getUniqueId('llb'));		
15111	
15112			// Finding thumbs
15113			thumbs = self.find(settings.delegate);
15114			
15115			if (!settings.useThumbstrip) {
15116				settings.prefsKept.splice(0, 1);
15117			}
15118			
15119			loadPrefs(settings, settings.prefsKept, 'lb_pref');
15120			
15121			// Initializing controls
15122			initLightbox();	
15123			
15124			// Setting up events
15125			
15126			// Resize
15127			$window.on('resize.' + ns /* + ' orientationchange.' + ns*/, function() {
15128					clearTimeout(resizeTimeout);
15129					// Executed only if 50ms has passed since the last resize event
15130					resizeTimeout = setTimeout(function() { 
15131						clearTimeout(resizeTimeout);
15132						if (main && main.length && !(isVr && isFullscreen())) {
15133							fitImage();
15134						}
15135						lazyloadThumbs(fitThumbstrip);
15136					}, 50);
15137				});
15138			
15139			// Quit on document touch
15140			if (settings.quitOnDocClick) {
15141				
15142				lb_overlay.on('click.' + ns, function(e) {
15143						
15144						if (listening) {
15145							if (settings.autohideControls && controlsHidden()) {
15146								// Toggle controls back
15147								showControls();
15148								return false;
15149							}
15150							
15151							if (main.length && !$(e.target).is(main)) {
15152								// Quit
15153								quitLightbox();
15154								return false;
15155							}
15156						}
15157						
15158						return true;
15159					});
15160			}
15161			
15162			// On mouse-driven devices
15163			if (settings.autohideControls) {
15164				
15165				// Auto hide controls
15166				$document.on('mousemove.' + ns, function() {
15167						showControls();
15168						hideControlsLater();
15169					});
15170			}
15171							
15172			// Keyboard
15173			if (settings.enableKeyboard) {
15174				
15175				$document.on('keyup.' + ns, function(e) {
15176					if (!listening || !main.length || lightbox.children('.modal:visible').length || document.activeElement && (document.activeElement.nodeName === 'INPUT' || document.activeElement.nodeName === 'TEXTAREA')) {
15177						return true;
15178					}
15179					
15180					e.preventDefault();
15181					
15182					switch (e.keyCode) {
15183						
15184						case 27:
15185							quitLightbox();
15186							break;
15187						
15188						case 33:
15189						case 37:
15190							previousImage();
15191							break;
15192							
15193						case 34:
15194						case 39:
15195							nextImage();
15196							break;
15197							
15198						case 97:
15199						case 35:
15200							loadImage(thumbs.length - 1);
15201							break;
15202							
15203						case 103:
15204						case 36:
15205							loadImage(0);
15206							break;
15207						
15208						case 106:
15209						case 179:
15210							toggleAuto();
15211							break;
15212							
15213						case 107:
15214							if (settings.useZoom) {
15215								if (settings.zoomSlider) {
15216									increaseZoom();
15217								} else {
15218									toggleZoom();
15219								}
15220							} else {
15221								toggleCaption();
15222							}
15223							break;
15224							
15225						case 109:
15226							if (settings.useZoom) {
15227								if (settings.zoomSlider) {
15228									decreaseZoom();
15229								} else {
15230									toggleZoom();
15231								}
15232							} else {
15233								toggleAll();
15234							}
15235							break;
15236							
15237						case 32:
15238							if (isVideo) {
15239								if (media[0].paused) {
15240									playVideo(media[0]);
15241								} else {
15242									media[0].pause();
15243								}
15244							} else if (isAudio) {
15245								if (media[0].paused) {
15246									media[0].play();
15247								} else {
15248									media[0].pause();
15249								}
15250							} else {
15251								toggleAuto();
15252							}
15253							break;
15254							
15255						default:
15256							return true;
15257											
15258					}
15259					
15260					return false;
15261				});
15262			}
15263			
15264			// Mouse wheel
15265			if (settings.mouseWheelAction) {
15266				var wheelTimeout = null;
15267				
15268				lightbox.on('mousewheel.' + ns + ' DOMMouseScroll.' + ns, function(e) {
15269						if (listening) {
15270							clearTimeout(wheelTimeout);
15271							
15272							if (	!main.length || 
15273									$(e.target).closest('.' + settings.lightboxCaptionClass).length ||
15274									main.hasClass('pdf') || 
15275									main.hasClass('external') ||
15276									lightbox.children('.modal:visible').length
15277								) {
15278								return true;
15279							}
15280							
15281							if (e.ctrlKey == true || settings.mouseWheelAction === 'zoom') {
15282								if (settings.useZoom) {
15283									if (settings.zoomSlider) {
15284										wheelTimeout = setTimeout((e.originalEvent.wheelDelta > 0 || e.originalEvent.detail < 0)?
15285											function(e) {
15286												increaseZoom();
15287											}
15288											: 
15289											function(e) {
15290												decreaseZoom();
15291											}, 100);
15292									} else {
15293										wheelTimeout = setTimeout((e.originalEvent.wheelDelta > 0 || e.originalEvent.detail < 0)?
15294											function(e) {
15295												if (!fitImages) {
15296													fitImages = true;
15297													fitImage(true);
15298												}
15299											}
15300											: 
15301											function(e) {
15302												if (fitImages) {
15303													fitImages = false;
15304													fitImage(true);
15305												}
15306											}, 100);
15307									}
15308								}
15309							} else if (settings.mouseWheelAction === 'navigation') {
15310								var d = new Date();
15311								if (typeof lightbox.data('lastWheel') === UNDEF || (d - lightbox.data('lastWheel')) > 250) {
15312									// Only if 250ms apart from the last wheel event
15313									wheelTimeout = setTimeout(function(dir) {
15314											lightbox.data('lastWheel', new Date());
15315											if (dir) {
15316												previousImage();
15317											} else {
15318												nextImage();
15319											}
15320									 	}, 100, (e.originalEvent.wheelDelta > 0 || e.originalEvent.detail < 0));
15321								}
15322							}
15323						}
15324						return false;
15325					});
15326			}
15327			
15328			// Events that can be triggered from outside
15329		
15330			// External trigger to switch image
15331			self.on('lightboxLoad.' + ns, function(e, n, auto) {
15332
15333						if (typeof n === NUMBER) {
15334							n = Math.minMax(0, n, thumbs.length);
15335						} else if (typeof n === OBJECT) {
15336							n = thumbs.index($(n));
15337						} else {
15338							n = 0;
15339						}
15340						
15341						if (typeof settings.onStart === FUNCTION) {
15342							settings.onStart(thumbs.eq(n));
15343						}
15344						
15345						if (slideshow || slideshowSuspended) {
15346							stopAuto(true);
15347						}
15348						
15349						loadImage(n, function() {
15350								// Executed when the image is loaded 
15351								if (auto) {
15352									startAuto();
15353									//hideControlsLater();
15354								}
15355							});
15356					})
15357				// External trigger to quit = does not remove, just hides the lightbox
15358				.on('lightboxQuit.' + ns, function() {
15359						quitLightbox();
15360						return false;
15361					})
15362				// Removing the lightbox completely
15363				.on('lightboxRemove.' + ns, function() {
15364						removeLightbox();
15365						return false;
15366					})
15367				// Stopping auto hide controls
15368				.on('stopAutoHide.' + ns, function() {
15369						clearTimeout(controlsTimeout);
15370					})
15371				// Returning the current lightbox container
15372				.on('lightboxContainer.' + ns, function() {
15373						return lightbox;
15374					})
15375				// External trigger to go to the previous image
15376				.on('lightboxNext.' + ns, function() {
15377						stopAuto(true);
15378						nextImage();
15379						return false;
15380					})
15381				// External trigger to go to next image
15382				.on('lightboxPrevious.' + ns, function() {
15383						stopAuto(true);
15384						previousImage();
15385						return false;
15386					});
15387			
15388			// Ready event
15389			
15390			if (typeof settings.onReady === FUNCTION) {
15391				settings.onReady(thumbs.eq(curr));
15392			}
15393			
15394			// Auto start
15395			
15396			if (settings.autoStart && settings.baseSet) {
15397				startAuto();
15398			}
15399	
15400			return this;
15401		};
15402	
15403	$.fn.lightbox.defaults = {
15404			delegate:						'.card.lbable',
15405			lightboxMainClass:				'lightbox-main',
15406			currClass:						'curr',
15407			lightboxCaptionClass:			'lightbox-caption',
15408			lightboxOnClass:				'lightbox-on',
15409			controlsClass:					'controls',
15410			buttonClass:					'button',
15411			lazyloadClass:					'lazyload',
15412			activeClass:					'active',
15413			scrollClass:					'scroll',
15414			regionsClass:					'regions',
15415			captionVisibleClass:			'caption-visible',
15416			thumbsVisibleClass:				'thumbs-visible',
15417			hasbuttonsClass:				'hasbuttons',
15418			immerseClass:					'immerse',
15419			controlsVisibleClass:			'controls-visible',
15420			controlsHideClass:				'controls-hide',
15421			checkboxClass:					'checkbox',
15422			checkmarkClass:					'icon-checkmark',
15423			selectedClass:					'checked',
15424			playingClass:					'playing',
15425			transitionProps:				'300ms ease-out',
15426			slideshowDelay:					4000,
15427			timingControl:					false,
15428			hideControlsDelay:				2500,
15429			prefsKept: 						[
15430												'thumbsVisible',
15431												'captionVisible',
15432												'audioClipVolume'
15433											],
15434			controlsUseText:				false,
15435			thumbsVisible:					true,
15436			useThumbstrip:					true,
15437			captionVisible:					true,
15438			fitImages:						true,
15439			fitBetween:						'screen',
15440			maxZoom:						1.0,
15441			horzPanoThreshold:				1.8,
15442			vertPanoThreshold:				0.5,
15443			displayOriginals:				true,
15444			useZoom:						true,
15445			zoomSlider:						false,
15446			useOriginals:					false,
15447			hiDpiThumbs:					false,
15448			hiDpiImages:					false,
15449			showFullscreen:					false,
15450			indexName:						'index.html',
15451			baseSet:						true,
15452			autohideControls:				false,
15453			autoStart:						false,
15454			clickAction:					'donothing',
15455			mouseWheelAction:				'navigation',
15456			useSlideshow:					true,
15457			autoplayAudioClips:				true,
15458			backgroundAudioSlideshowControl:false,
15459			muteBackgroundAudio:			true,
15460			use360Player:					true,
15461			autoRotate360:					true,
15462			rotateSpeed360:					-2,
15463			useGoogleDocs:					true,
15464			lightboxFullscreen:				false,
15465			lightboxFullscreenOnlyMobiles:	false,
15466			slideshowFullscreen:			false,
15467			slideshowFullscreenOnlyMobiles:	true,
15468			afterLast:						'donothing',
15469			avoidNavigationOnZoomed:		true,
15470			doubleClickZoom:				true,
15471			mapHeight:						0.8,
15472			mapApiKey:						'',
15473			mapType:						'hybrid',
15474			mapZoom:						15,
15475			showShop:						false,
15476			showFeedback:					false,
15477			showLowestPrice:				false,
15478			videoAuto:						false,
15479			videoLoop:						false,
15480			autoPano:						false,
15481			autoPanoStart:					0,
15482			autoPanoSpeed:					80,
15483			volume:							0.5,
15484			audioClipVolume:				0.75,
15485			rightClickProtect: 				false,
15486			useRating:						false,
15487			jalbumRating:					false,
15488			visitorRating:					false,
15489			buttonLabels:					true,
15490			showNumbers:					false,
15491			infoPanelAdapt:					false,
15492			showShare:						false,
15493			showDownload:					false,
15494			allowDownloadScaled:			false,
15495			allowDownloadOthers:			false,
15496			showMap:						false,
15497			showRegions:					true,
15498			regionsVisible:					false,
15499			printImage:						false,
15500			metaAsPopup:					true,
15501			transitionType:					'crossFadeAndSlide',
15502			transitionSpeed:				400,
15503			panoramaAutoDetect:				1.3333,
15504			panoramaTreshold:				[ 2.5, 2.5 ],
15505			defaultPosterSize:				[ 628, 360 ],
15506			thumbSpeed:						400,
15507			thumbOverhead:					40,
15508			preloadNext:					true,
15509			enableKeyboard: 				true,
15510			enableMouseWheel:				true,
15511			quitOnEnd:						true,
15512			quitOnDocClick: 				true,
15513			onStart:						false,
15514			onClose:						false,
15515			onLoadStart:					false,
15516			onLoadEnd:						false,
15517			onAfterLast:					false,
15518			onBeforeFirst:					false,
15519			onReady:						false,
15520			onSlideshowStart:				false,
15521			onSlideshowPause:				false,
15522			onVolumeChange:					false,
15523			feedbackBtnLabel:				'Write feedback',
15524			feedbackBtnTooltip:				'View feedback window'
15525		};
15526	
15527	$.fn.lightbox.text = {
15528			rating:							"Rating",
15529			print:							"Print",
15530			printLabel:						"Print out this photo on your printer",
15531			download:						"Download",
15532			addCart:						"Add to Cart",
15533			shopLabel:						"Add this item to the shopping cart",
15534			viewCart:						"View Cart",
15535			viewCartLabel:					"View shopping cart",
15536			fotomotoBtn:					"Buy / Share",
15537			fotomotoTooltip:				"Buy prints or digital files, share, send free eCards",
15538			metaBtn:						"Photo data",
15539			metaLabel:						"Display photographic (Exif/Iptc) data",
15540			regionsBtn:						"People",
15541			regionsLabel:					"Show tagged people",
15542			mapBtn:							"Map",
15543			mapLabel:						"Show the photo location on the map",
15544			mostphotosBtn:					"Purchase",
15545			mostphotosLabel:				"Download this image from <b>mostphotos.com</b>!",
15546			share:							"Share",
15547			shareLabel:						"Share this photo over social sites",
15548			shareOn:						"Share on",
15549			play:							"Play",
15550			audioClipLabel:					"Play / pause the attached audio clip",
15551			startOver:						"Start over",
15552			upOneLevel:						"Up one level",
15553			backToHome:						"Back to home",
15554			backToIndex:					"Back to index page",
15555			nextFolder:						"Next folder",
15556			nextIndex:						"Next index page",
15557			atLastPageQuestion:				"Where to go next?",
15558			atLastPage:						"At last page",
15559			clickToOpen:					"Click to open this document with the associated viewer",
15560			pauseShort:						"Pause",
15561			startSlideshowShort:			"Play",
15562			previousPicture:				"Previous picture",
15563			previousPictureShort:			"Prev",
15564			upOneLevelShort:				"Exit",
15565			zoom:							"Zoom",
15566			oneToOneSize:					"1:1 size",
15567			oneToOneSizeShort:				"1:1",
15568			fitToScreen:					"Fit to screen",
15569			fitToScreenShort:				"Fit",
15570			showThumbs:						"Show thumbnails",
15571			showThumbsShort:				"Thumbnails",
15572			hideThumbs:						"Hide thumbnails",
15573			hideThumbsShort:				"Hide thumbs",
15574			showInfo:						"Show caption / info",
15575			showInfoShort:					"Info",
15576			hideInfo:						"Hide caption / info",
15577			hideInfoShort:					"Hide info",
15578			slideshow:						"Slideshow",
15579			timing:							"Timing",
15580			startStopSlideshow:				"Start / stop slideshow",
15581			fullscreen:						"Fullscreen",
15582			fullscreenShort:				"Fullscr",
15583			exitFullscreen:					"Exit fullscreen",
15584			exitFullscreenShort:			"Normal",
15585			nextPicture:					"Next picture",
15586			nextPictureShort:				"Next"
15587		};
15588
15589	
15590})(jQuery, jQuery(window), jQuery(document));
15591/* 
15592 *	main.js - the skin's javascript functions
15593 */
15594	
15595;(function($, $window, $document) {
15596	'use strict';
15597				
15598	var $body = $(document.body);
15599	
15600	/********************************************************
15601	 *
15602	 *		Implementing select functions on images
15603	 *
15604	 ********************************************************/
15605	
15606	$.fn.selectable = function(options) {
15607		
15608			options = $.extend({
15609								cardClass:				'card',
15610								thumbClass:				'thumb',
15611								checkboxClass: 			'checkbox',
15612								selectedClass:			'checked',
15613								checkmarkClass:			'icon-checkmark',
15614								hasSelectedClass:		'has-selected',
15615								allSelectedClass:		'all-selected',
15616								selectionChange:		null
15617							}, options);
15618			
15619			var overlay = $(this).eq(0),
15620				cards,
15621				
15622				update = function(n) {
15623					
15624						if (typeof options.selectionChange === FUNCTION) {
15625							options.selectionChange.call(overlay[0], n);
15626						}
15627						
15628						$body
15629							.toggleClass(options.hasSelectedClass, n > 0)
15630							.toggleClass(options.allSelectedClass, n === cards.length);
15631					},
15632					
15633				selectedCnt = function() {
15634						return cards.filter('.' + options.selectedClass).length;
15635					},
15636			
15637				getCards = function() {
15638						return options.selector? overlay.find(options.selector) : overlay.find('.' + options.cardClass);
15639					},
15640			
15641				selectCard = function(e) {
15642						var card = (e instanceof $)? e : $(this);
15643						
15644						card.addClass(options.selectedClass)
15645							.find('.' + options.checkboxClass)
15646							.addClass(options.checkmarkClass);
15647					},
15648					
15649				unselectCard = function(e) {
15650						var card = (e instanceof $)? e : $(this);
15651						
15652						card.removeClass(options.selectedClass)
15653							.find('.' + options.checkboxClass)
15654							.removeClass(options.checkmarkClass);
15655					},
15656					
15657				toggleCard = function(e) {
15658						var card = (e instanceof $)? e : $(this);
15659						
15660						if (card.hasClass(options.selectedClass)) {
15661							unselectCard(card);
15662						} else {
15663							selectCard(card);
15664						}
15665					},
15666			
15667				clicked = function(e) {
15668						var card = $(e.target).closest('.' + options.cardClass),
15669							n = selectedCnt();
15670						
15671						if (card.hasClass(options.selectedClass)) {
15672							unselectCard(card);
15673							n -= 1;
15674						} else {
15675							selectCard(card);
15676							n += 1;
15677						}	
15678							
15679						update(n);
15680						
15681						return false;
15682					},
15683			
15684				selectAll = function() {
15685					
15686						if (overlay.is(':visible')) {
15687							var n = selectedCnt();
15688							
15689							if (n < cards.length) {
15690								n = cards.length;
15691								cards.not('.' + options.selectedClass).each(function() {
15692										selectCard($(this));
15693									});
15694							}
15695							
15696							update(n);
15697						}
15698					},
15699			
15700				selectNone = function() {
15701					
15702						if (overlay.is(':visible')) {
15703							var n = selectedCnt();
15704							
15705							if (n > 0) {
15706								n = 0;
15707								cards.filter('.' + options.selectedClass).each(function() {
15708										unselectCard($(this));
15709									});
15710							}
15711							
15712							update(n);
15713						}
15714					},
15715					
15716				selectInverse = function() {
15717					
15718						if (overlay.is(':visible')) {
15719							var n = cards.length - selectedCnt();
15720							
15721							if (n === 0) {
15722								// All selected
15723								cards.each(function() {
15724										unselectCard($(this));
15725									});								
15726							} else {
15727								// Some selected
15728								cards.each(function() {
15729										toggleCard($(this));
15730									});
15731							} 
15732							
15733							update(n);
15734						}
15735					},
15736				
15737				selectRefresh = function() {
15738						var th, 
15739							cb;
15740							
15741						cards = getCards();
15742						
15743						cards.each(function() {
15744								
15745								if (!($(this).find('span.' + options.checkboxClass).length)) {
15746									
15747									th = $(this).children('.' + options.thumbClass).eq(0);
15748									
15749									if (th.length) {
15750										cb = $('<span>', {
15751												'class': 	options.checkboxClass
15752											}).appendTo(th);
15753									} else {
15754										cb = $('<span>', {
15755												'class': 	options.checkboxClass
15756											}).prependTo($(this));
15757									}
15758									
15759									cb.on({
15760											click: 			clicked
15761										});
15762									
15763									$(this).on({
15764											selectCard:		selectCard,
15765											unselectCard:	unselectCard,
15766											toggleCard:		toggleCard
15767										});
15768								}
15769								
15770							});
15771						
15772						if (typeof options.selectionChange === FUNCTION) {
15773							options.selectionChange.call(overlay[0], selectedCnt());
15774						}
15775						
15776					};
15777			
15778			selectRefresh();
15779			
15780			overlay.on({
15781					selectAll: 			selectAll,
15782					selectNone: 		selectNone,
15783					selectInverse: 		selectInverse,
15784					selectRefresh: 		selectRefresh
15785				});
15786					
15787			return this;
15788		};
15789			
15790		
15791	/********************************************************
15792	 *
15793	 *					The main skin code
15794	 *
15795	 ********************************************************/
15796	
15797	$.fn.skin = function(settings) {
15798		
15799			settings = $.extend({}, $.fn.skin.defaults, settings);
15800			
15801			var main					= 	$(this),								// Main SECTION
15802				content					=	main.find('#main-cont'),				// Main container
15803				text 					= 	getTranslations({						// Translated texts
15804													'return':						"return",
15805													rendering:						"Rendering",
15806													addCart:						"Add to Cart",
15807													fotomotoBtn:					"Buy / Share",
15808													fotomotoTooltip:				"Buy prints or digital files, share, send free eCards",
15809													audioClipLabel:					"Play / pause the attached audio clip",
15810													selectedItems:					"Selected items",
15811													label:							"Label",
15812													search:							"Search",
15813													searchNew:						"Search new",
15814													loading:						"Loading",
15815													foundNTimes:					"{0} image(s)",
15816													foundTooMany:					"{0} images; displaying only the first {1}",
15817													notFound:						"not found",
15818													searchPlaceholder:				"Search...",
15819													more:							"more",
15820													less:							"less",
15821													imagesTaken:					"Images taken",
15822													imagesModified:					"Images modified",
15823													imagesAdded:					"Images added",
15824													betweenDays:					"between {0} and {1}",
15825													onDay:							"on {0}",
15826													afterDay:						"after {0}",
15827													beforeDay:						"before {0}",
15828													sinceMyLastVisit:				"since my last visit",
15829													results:						"Results",
15830													sortedBy:						"Sorted by",
15831													ascending:						"ascending",
15832													descending:						"descending",
15833													noRating:						"No rating",
15834													multipleSelectHint:				"Use SHIFT to select range, CTRL for multiple entries",
15835													select:							"Select",
15836													reset:							"Reset",
15837													sortBy:							"Sort by",
15838													image:							"image",
15839													images:							"images",
15840													audio:							"audio",
15841													audios:							"audios",
15842													video:							"video",
15843													videos:							"videos",
15844													other:							"other",
15845													others:							"others",
15846													fatalError:						"Fatal error!"
15847												}),
15848			
15849				shopRoot 				=	$(),										// Root element for cart
15850				shopBox 				= 	$(),										// Whole Shopping section
15851				feedbackRoot 			=	$(),										// Root element for the feedback tool
15852				feedbackBox 			=	$(),										// Whole Feedback section
15853				mapRoot,																// Root element for map
15854				album,																	// The JSON database
15855				ns 						= 	'tiger_skin',								// Namespace
15856				lastHash 				= 	'',											// Storing last state
15857				hashChangedByJs 		= 	false,										// Tracking for internally or externally triggered hash changes
15858				pageExt					= 	settings.indexName.split('.')[1] || 'html',	// Index page extension
15859				initByHash 				= 	window.location.hash !== '',				// Initial URL contains hash
15860				nows 					=	new Date() / 1000,							// Now in seconds
15861				currency 				=	'EUR',
15862				scrollTop 				= 	$window.scrollTop(),						// initial scroll top
15863				
15864				// Determine card type
15865				
15866				cardType = function(card) {
15867						var s = card.closest('section');
15868						if (!s.length) {
15869							return 'image';
15870						} else if (s.hasClass(settings.folderClass)) {
15871							return 'folder';
15872						} else if (s.hasClass(settings.thumbnailsClass)) {
15873							return 'thumb';
15874						}
15875						
15876						return '';
15877					},
15878												
15879				/********************************************************
15880				 *
15881				 *					Managing overlays
15882				 *				e.g. after search or filter
15883				 *
15884				 ********************************************************/
15885				/*
15886				
15887					<section id="main" class="content"> => main
15888						
15889						// Base only
15890						<article id="main-cont"> 														<!-- content -->
15891							<section class="thumbnails base-overlay" data-overlay="base">				<!-- base overlay -->
15892								<div class="cont horizontal caption-over hover-zoom caption-on-hover">	<!-- container -->
15893								</div>
15894							</section>
15895							<section id="tag-cloud" class="row cont-box"><div class="cont box"><h4 class="icon-label"> Labels</h4><div class="tag-cloud"></div><form class="tag-search" data-depth="tree" data-fields="creator,keywords,folder:title,folder:name,webLocation:title,webPage:title" wtx-context="94581790-1AAD-4D31-91BA-67ADA9D1EC6F"><div class="search-input"><input type="text" placeholder="Search..." aria-label="Search" wtx-context="46A4B41B-684F-4DFB-8E45-6D8FAC4C5691"></div><a class="search-btn icon-search"></a></form></div></section>
15896							<section class="prev-next-links"><div class="cont"><div class="next-link has-bg" style="background-image:url(../People/thumbs/Newborn.jpg)"><a rel="next" href="../People/index.html"><span class="caption">People <span class="icon-caret-right"></span></span></a></div></div></section>
15897						</article>
15898						
15899						// Base + overlay
15900						<article id="main-cont" class="has-overlay"> 									<!-- content -->
15901							<section class="thumbnails base-overlay" data-overlay="base">				<!-- base overlay -->
15902								<div class="cont horizontal caption-over hover-zoom caption-on-hover">	<!-- container -->
15903								</div>
15904							</section>
15905							<section class="thumbnails overlay" data-overlay="oly_NNNN">				<!-- overlay -->
15906								<header class="cont overlay-title icon-filter">							<!-- header -->
15907									<h4>Title</h4>
15908									<a class="close">Return</a>
15909								</header>
15910								<div class="cont horizontal caption-over hover-zoom caption-on-hover">	<!-- container -->
15911								</div>
15912							</section>
15913							<section id="tag-cloud" class="row cont-box"><div class="cont box"><h4 class="icon-label"> Labels</h4><div class="tag-cloud"></div><form class="tag-search" data-depth="tree" data-fields="creator,keywords,folder:title,folder:name,webLocation:title,webPage:title" wtx-context="94581790-1AAD-4D31-91BA-67ADA9D1EC6F"><div class="search-input"><input type="text" placeholder="Search..." aria-label="Search" wtx-context="46A4B41B-684F-4DFB-8E45-6D8FAC4C5691"></div><a class="search-btn icon-search"></a></form></div></section>
15914							<section class="prev-next-links"><div class="cont"><div class="next-link has-bg" style="background-image:url(../People/thumbs/Newborn.jpg)"><a rel="next" href="../People/index.html"><span class="caption">People <span class="icon-caret-right"></span></span></a></div></div></section>
15915						</article>
15916						
15917					</section>
15918				*/
15919				
15920				OLYTAG = 'section',
15921				
15922				// Getting active overlay; creating one if missing
15923				
15924				getOverlay = function(create) {
15925						var oly = content.find('.' + settings.thumbnailsClass).last(
15925);
15926						
15927						if (!oly.length && typeof create !== UNDEF && create) {
15928							oly = createOverlay();
15929						}
15930						
15931						return oly;
15932					},
15933					
15934				// Getting base overlay
15935				
15936				getBaseOverlay = function() {
15937				
15938						return content.find('.' + settings.baseOverlayClass);
15939					},
15940					
15941				// Content has overlay?
15942				
15943				hasOverlay = function() {
15944					
15945						return content.find('.' + settings.overlayClass + ':visible').length > 0;
15946					},
15947					
15948				// Is this the base overlay?
15949				
15950				isBaseOverlay = function(oly) {
15951						
15952						return (typeof oly === UNDEF? getOverlay() : oly).hasClass(settings.baseOverlayClass);
15953					},
15954					
15955				// Getting NameSpace, creating new if not exists
15956				
15957				getOverlayNS = function(oly) {
15958						oly = typeof oly === UNDEF? getOverlay() : oly;
15959						
15960						if (oly.length) {
15961							var ns = oly.data('overlay');
15962						
15963							if (ns) {
15964								return ns;
15965							}
15966						
15967							ns = oly.hasClass(settings.baseOverlayClass)? 'base' : getUniqueId('oly');
15968							
15969							oly.attr('data-overlay', ns);
15970					
15971							return ns;
15972						}
15973							
15974						return '';
15975					},
15976					
15977				// Setting NameSpace
15978				
15979				setOverlayNS = function(ns, oly) {
15980						oly = typeof oly === UNDEF? getOverlay() : oly;
15981						ns = typeof ns === UNDEF? (oly.hasClass(settings.baseOverlayClass)? 'base' : getUniqueId('oly')) : ns;
15982						
15983						oly.attr('data-overlay', ns);
15984						
15985						return ns;
15986					},
15987					
15988				// Scroll to overlay
15989					
15990				focusOverlay = function(oly) {
15991						oly = typeof oly === UNDEF? getOverlay() : oly;
15992						
15993						if (oly.length && (oly.offset().top < $window.scrollTop() || oly.offset().top > ($window.scrollTop() + $window.height() - 100))) {
15994							$window.scrollTop(oly.offset().top);
15995						}
15996					},
15997					
15998				// Creating a new overlay
15999				
16000				createOverlay = function() {
16001						var ns = getUniqueId('oly'),
16002							oly = $('<' + OLYTAG + '>', {
16003										'class':		settings.thumbnailsClass + ' ' + settings.overlayClass
16004									}).attr({
16005										id:				ns,
16006										'data-overlay':	ns
16007									}),
16008							boly = getBaseOverlay();									// Base overlay
16009							
16010						if (boly.length) {
16011							// Has base: create after it
16012							boly.after(oly);
16013						} else {
16014							// No base: create after the folders if exists
16015							boly = content.find('.' + settings.foldersClass);
16016							if (boly.length) {
16017								boly.after(oly);
16018							} else {
16019								// No folders: create at first position
16020								content.prepend(oly);
16021							}
16022						}
16023						
16024						createImageContainer(oly);
16025						
16026						content.addClass(settings.hasOverlayClass);
16027						
16028						return oly;
16029					},
16030					
16031				// Creating header
16032				
16033				createOverlayHeader = function(options, oly) {
16034						var head = $('<header>', {
16035									 'class':	[	settings.contClass,
16036									 	 			settings.overlayTitleClass,
16037									 	 			(options && options.hasOwnProperty('icon'))? options.icon : ''
16038									 	 		].filter(Boolean).join(' ')
16039								}).prependTo(typeof oly === UNDEF? getOverlay() : oly);
16040						
16041						if (options && options.hasOwnProperty('title')) {
16042							head.append($('<h' + settings.overlayHeadLevel + '>', {
16043									html: 	options.title
16044								}));
16045						}			
16046								
16047						head.append($('<a>', {
16048									'class': 	'close',
16049									href: 		'',
16050									text: 		text.return
16051								})
16052							.on('click', function() {
16053									removeOverlay();
16054									return false;
16055								}));
16056						
16057						return head;
16058					},
16059					
16060				// Getting overlay header
16061				
16062				getOverlayHeader = function() {
16063						
16064						return getOverlay().children('header').eq(0);
16065					},
16066					
16067				// Getting overlay title
16068				
16069				getOverlayTitle = function() {
16070						
16071						return getOverlayHeader().find('h' + settings.overlayHeadLevel).eq(0);
16072					},
16073					
16074				// Append text to overlay title
16075				
16076				appendToOverlayTitle = function(cont) {
16077						var t = getOverlayTitle();
16078						
16079						return t.length? cont.appendTo(t) : $();
16080					},
16081					
16082				// Adding progressbar
16083				progressAnimateTo,
16084				
16085				appendProgressbar = function(title) {
16086						var p = getOverlayHeader().find('.' + settings.progressClass);
16087						
16088						if (p.length) {
16089							p.empty();
16090						} else {
16091							p = $('<div>', {
16092									'class': 	settings.progressClass
16093								}).insertAfter(getOverlayTitle());
16094						}
16095						
16096						if (typeof title !== UNDEF && title) {
16097							p.append('<div class="title">' + title + '</div>');
16098						}
16099						
16100						p.append('<div class="' + settings.progressbarClass + '"><span></span></div>');
16101						
16102						// Auto animate if no setProgressbar() within 500ms
16103						progressAnimateTo = setTimeout(function() {
16104								p.addClass('animate');
16105							}, 500);
16106						
16107						return p;
16108					},
16109					
16110				// Set progress to 0..1
16111				
16112				setProgressbar = function(c) {
16113						clearTimeout(progressAnimateTo);
16114						var p = getOverlayHeader().find('.' + settings.progressClass);
16115						p.removeClass('animate');
16116						p.find('.' + settings.progressbarClass + ' span').width(c * 100 + '%');
16117					},
16118					
16119				// Remove progressbar
16120				
16121				removeProgressbar = function(target) {
16122						getOverlayHeader().find('.' + settings.progressClass).remove();
16123					},
16124
16125				// Associate elements with the current overlay
16126				
16127				attachToOverlay = function(el) {
16128						if (el.length) {
16129							var ns = getOverlayNS();
16130							
16131							el.each(function() {
16132									$(this).attr('data-rel', ns);
16133								});
16134						}
16135					},
16136					
16137				// Remove overlay-attached elements	
16138					
16139				removeAttachedElementsFromOverlay = function(ns) {
16140						if (!ns) {
16141							ns = getOverlayNS();
16142						}
16143						
16144						content.find('[data-rel=' + ns + ']').remove();
16145					},
16146				
16147				// Creating image container within target overlay
16148				
16149				createImageContainer = function(oly) {
16150						if (!oly) {
16151							oly = getOverlay();
16152						}
16153							
16154						return $('<div>', {
16155									'class': 	[
16156													settings.contClass,
16157													settings.thumbLayout, 
16158													'caption-' + settings.captionPlacement,
16159													(settings.captionPlacement !== 'below' && settings.captionShowOnHover)? 'caption-on-hover' : ''
16160												].filter(Boolean).join(' ')
16161									}).appendTo(oly);
16162					},
16163					
16164				// Getting base thumbnail container 
16165				
16166				getBaseImageContainer = function() {
16167						return content
16168								.find('.' + settings.thumbnailsClass + 
16169									':not(.' + settings.overlayClass + ')')
16170								.first()
16171								.children('div.' + settings.contClass);
16172					},
16173					
16174				// Returns the current thumbnail container if exists
16175				
16176				getImageContainer = function(create) {
16177						var ic = content
16178									.find('.' + settings.thumbnailsClass)
16179									.last()
16180									.children('div.' + settings.contClass);
16181						
16182						if (!ic.length && create) {
16183							ic = createImageContainer(getOverlay(true));
16184						}
16185						
16186						return ic; 
16187					},
16188					
16189				// Restoring base overlay's attached elements
16190				
16191				restoreBaseOverlay = function() {
16192						var bo = getBaseOverlay();
16193						
16194						if (bo.length) {
16195							// Attached elements
16196							$('[data-rel=' + bo.data('overlay') + ']').not('.hidden').show();
16197							// Refreshing thumbnails
16198							bo.trigger('refresh');
16199							// Refreshing folders too
16200							lazyloadFolders();
16201							// Refreshing the map
16202							getBaseMap().trigger('refresh');
16203						}
16204					},
16205					
16206				// Getting map on the base layer
16207				
16208				getBaseMap = function() {
16209						return getBaseOverlay().find('.' + settings.mapRootClass);
16210					},
16211										
16212				// Getting all base cards
16213				
16214				getBaseCards = function(selector) {
16215						return getBaseImageContainer()
16216								.find('.' + settings.cardClass + (selector || ''));
16217					},
16218					
16219				// Returns the current cards (base or overlay)
16220				
16221				getCards = function(selector) {
16222						return getImageContainer()
16223								.find('.' + settings.cardClass + (selector || ''));
16224					},
16225					
16226				// Returns base thumbnails
16227				
16228				getBaseImages = function(selector) {
16229						return getBaseImageContainer()
16230								.find('.' + settings.cardClass + 
16231									' .' +	settings.thumbClass + 
16232									(selector || ''));
16233					},
16234					
16235				// Returns the current thumbnails (base or overlay)
16236				
16237				getImages = function(selector) {
16238						return getImageContainer()
16239								.find('.' + settings.cardClass + 
16240									' .' + settings.thumbClass + 
16241									(selector || ''));
16242					},
16243					
16244				// Returns all videos (base or overlay)
16245				
16246				getVideos = function(selector) {
16247						return getOverlay()
16248								.find('.' + settings.cardClass + ' video' + (selector || ''));
16249					},
16250					
16251				// Returns the current cards (base or overlay)
16252				
16253				getSelectedCards = function() {
16254						return getImageContainer()
16255								.find('.' + settings.cardClass + 
16256									'.' + settings.selectedClass);
16257					},
16258					
16259				// Getting current card
16260				
16261				getActiveCard = function() {
16262						return getImageContainer()
16263								.find('.' + settings.cardClass + 
16264									'.' + settings.activeClass);
16265					},
16266					
16267				// Returns the current cards (base or overlay)
16268				
16269				getSelectableCards = function() {
16270						return getImageContainer()
16271								.find('.' + settings.cardClass + 
16272									'.' + settings.selectableClass);
16273					},
16274					
16275				// Removing overlay
16276				
16277				removeOverlay = function() {
16278						var oly = getOverlay();
16279						
16280						if (oly.length && !isBaseOverlay()) {
16281							var ns = oly.data('overlay');
16282							
16283							$window.off('.' + ns);
16284							
16285							// Removing the lightbox
16286							oly.trigger('removeLightbox');
16287							
16288							// Removing related elements
16289							removeAttachedElementsFromOverlay(ns);
16290							main.removeClass(settings.noMapClass + ' ' + settings.hasMapClass);
16291							
16292							oly.trigger('overlayRemoved');
16293							
16294							oly.remove();
16295							
16296							content.removeClass(settings.hasOverlayClass);
16297							
16298							restoreBaseOverlay();
16299							
16300							if (settings.hasOwnProperty('search')) {
16301								emptySearchBox();
16302							}
16303							
16304							setHash();
16305							
16306							updateShares();
16307							
16308							if (!getSelectableCards().length) {
16309								$('.hide-on-empty').hide();
16310							}
16311							
16312							content.hideAllTooltips();
16313							content.find('[data-sticky-rel]').trigger('stickyRefresh');
16314						}
16315					},
16316					
16317				// Adding new overlay
16318			
16319				addOverlay = function(options) {
16320						
16321						// Removing existing overlay first
16322						if (hasOverlay()) {
16323							removeOverlay();
16324						}
16325						
16326						content.hideAllTooltips();
16327						
16328						// Creating overlay (section.thumbnails)
16329						var oly = createOverlay();
16330						
16331						// Creating header
16332						createOverlayHeader(options, oly);
16333							
16334						// Triggering ready event
16335						content.trigger('overlayReady', oly);
16336						
16337						content.find('[data-sticky-rel]').trigger('stickyRefresh');
16338						
16339						return oly;
16340					},
16341				
16342				// Getting file name from thumbnail
16343				
16344				getImageFileName = function(thumb) {
16345						var l = thumb.data('href') || thumb.attr('href');
16346								
16347						return l? l.substring(l.lastIndexOf('/') + 1).replace('#img=', '') 
16347: '';
16348					},
16349				
16350				// Checking if file is a custom page and matches base name: "name"
16351				
16352				checkPageName = function(fn, name) {
16353						
16354						if (fn.slice(-pageExt.length) === pageExt) {
16355							return fn.substring(0, fn.lastIndexOf('.')) === name.substring(0, name.lastIndexOf('.'));
16356						}
16357						
16358						return false;
16359					},
16360					
16361				// Getting image by file name, name is mandatory
16362				
16363				getCardByName = function(name, base) {
16364						var cards,
16365							t = $();
16366						
16367						cards = (typeof base === UNDEF || !base)?
16368							getCards()
16369							:
16370							getBaseCards();
16371						
16372						name = decodeURIComponent(name);
16373						
16374						cards.find('.' + settings.thumbClass).each(function() {
16375								var  fn = $(this).data('name') || getImageFileName($(this));
16376								
16377								if (fn === name || checkPageName(fn, name)) {
16378									t = $(this).closest('.' + settings.cardClass);
16379									return false;
16380								}
16381							});
16382						
16383						return t;
16384					},
16385				
16386				// Getting folders container
16387				
16388				getFolderContainer = function() {
16389						return content.find('.' + settings.foldersClass + ' .' + settings.contClass);
16390					},
16391										
16392				// Getting all folder cards
16393				
16394				getFolders = function(sel) {
16395						var fc = getFolderContainer();
16396						
16397						return fc.length? fc.find('.' + settings.cardClass + (sel || '')) : $();
16398					},
16399					
16400				// Getting map
16401				
16402				getMapRoot = function(create) {
16403						var mapbox =  content.find(settings.mapBoxHook);
16404						
16405						if (settings.pageType !== 'page') {
16406							// Finding only the map related to the current overlay 
16407							var ns = getOverlayNS();
16408							mapbox = mapbox.filter(function() { return $(this).data('rel') === ns; });
16409						}
16410						
16411						if (mapbox.length && mapbox.is(':visible')) {
16412							
16413							return mapbox.find('.' + settings.mapRootClass);
16414							
16415						} else if (create) {
16416							
16417							var newMap = $('<section class="map show-on-overlay">' +
16418												'<div class="cont box">' +
16419													'<div class="' + settings.mapRootClass + '" data-map-root></div>' +
16420												'</div>' +
16421											'</section>');
16422						
16423							if (mapbox.length) {
16424								mapbox.after(newMap);
16425							} else {
16426								getOverlay().before(newMap);
16427							}
16428							
16429							attachToOverlay(newMap);
16430							
16431							return newMap.find('.' + settings.mapRootClass);
16432						}
16433						
16434						return $();
16435					},
16436													
16437				// Getting attached lightbox
16438				
16439				getLightbox = function() {
16440						return getImageContainer().find('.' + settings.lightboxClass).last();
16441					},
16442					
16443				// Is lightbox on?
16444				
16445				lightboxOn = function() {
16446						return getLightbox().is(':visible');
16447					},
16448					
16449				// Loading an image in lightbox
16450				
16451				lightboxLoad = function(card, auto) {
16452						pausePlayer(getImages(' .playing'));
16453						getImageContainer().trigger('lightboxLoad', (typeof auto === UNDEF)? card : [ card, auto ]);
16454					},
16455					
16456				// Leaving lightbox
16457				
16458				lightboxQuit = function() {
16459						getImageContainer().trigger('lightboxQuit');
16460					},
16461					
16462				// Getting selected items from current overlay
16463				
16464				getSelectedItems = function() {
16465						var r = [];
16466								
16467						getSelectedCards().each(function() {
16468								r.push($(this).data(J.OBJ));
16469							});
16470						
16471						return r;
16472					},
16473				
16474				// Getting all items from current overlay
16475				
16476				getAllItems = function() {
16477						var r = [];
16478								
16479						getCards().each(function() {
16480								var i = $(this).data(J.OBJ); 
16481								if (i) r.push(i);
16482							});
16483						
16484						return r;
16485					},
16486								
16487				// Getting all items from base overlay
16488				
16489				getBaseItems = function() {
16490						var r = [];
16491						
16492						getBaseCards().each(function() {
16493								var i = $(this).data(J.OBJ);
16494								if (i) r.push(i);
16495							});
16496						
16497						return r;
16498					},
16499					
16500				// Getting active element's data
16501				
16502				getCurrentItem = function() {
16503						var card = getActiveCard();
16504						
16505						if (card.length) {
16506							return card.data(J.OBJ);
16507						}
16508						
16509						return null;
16510					},
16511									
16512				/********************************************************
16513				 *
16514				 *					Managing buttons
16515				 *
16516				 ********************************************************/
16517				 
16518				// Toggle select buttons: selected count, max count
16519				
16520				toggleSelectButtons = function(sc, mc) {
16521						var p = shopRoot.add(feedbackRoot);
16522						
16523						if (typeof mc === UNDEF || !mc) {
16524							p.find('.' + settings.selectBoxClass).hide();					
16525						} else {
16526							p.find('.' + settings.selectBoxClass).show();
16527							p.find('.' + settings.selectNoneClass + 
16528									',.' + settings.addSelectedClass + 
16529									',.' + settings.keepSelectedClass).toggleClass(settings.disabledClass, typeof sc === UNDEF || sc === 0);
16530							
16531							p.find('.' + settings.selectAllClass).toggleClass(settings.disabledClass, typeof mc !== UNDEF && sc === mc);
16532						}
16533					},
16534						
16535				// Toggle boxes according to item count
16536				
16537				toggleBoxes = function(mc) {
16538						feedbackRoot.add(shopRoot).toggleClass(settings.disabledClass, !mc);
16539					},
16540					
16541				// Toggling 'has-images' class
16542				
16543				toggleHasImages = function(n) {
16544						main.toggleClass(settings.hasImagesClass, n).toggleClass(settings.noImagesClass, !n);
16545					},
16546						
16547				// Emptying the Search box
16548				
16549				emptySearchBox = function() {
16550						$(settings.search.hook).find('input').val('');
16551					},
16552					
16553				/********************************************************
16554				 *
16555				 *					Audio clip handling
16556				 *
16557				 ********************************************************/
16558				
16559				// Creating the AUDIO element
16560				
16561				createPlayer = function(btn) {
16562						var	player = $('<audio>', {
16563									'class':		'audioclip-player',
16564									src:			btn.attr('href'),
16565									id:				btn.attr('rel'),			
16566									controlsList:	'nofullscreen nodownload noremote noplaybackrate'
16567								}).hide().insertAfter(btn);
16568								
16569						player[0].volume = settings.audioClipVolume;
16570						
16571						return player;		
16572					},
16573					
16574				// Audio
16575					
16576				togglePlayer = function(e) {
16577						var btn = $(this),
16578							pl;
16579						
16580						e.preventDefault();
16581						
16582						// Initialized?
16583						if (btn[0].hasAttribute('rel')) {
16584							pl = $('#' + btn.attr('rel'));
16585						}
16586						
16587						// Has AUDIO element?
16588						if (!pl || !pl.length) {
16589							// not yet created
16590							btn.attr('rel', getUniqueId('ply'));
16591							pl = createPlayer(btn);
16592						}
16593						
16594						if (pl[0].paused) {
16595							
16596							pausePlayers();
16597							
16598							// Paused: trying to start
16599							pl[0].play().then(function() {
16600									btn.removeClass('icon-volume-up paused').addClass('icon-pause playing');
16601									pl.on('ended', function() {
16602											btn.removeClass('icon-pause playing').addClass('icon-volume-up paused');
16603										});
16604								}, function(err) {
16605									btn.removeClass('icon-pause playing').addClass('icon-volume-up paused');
16606									console.log(err);
16607								});
16608						} else {
16609							pl[0].pause();
16610							btn.removeClass('icon-pause playing').addClass('icon-volume-up paused');
16611						}
16612						
16613						return false;
16614					},
16615					
16616				pausePlayers = function() {
16617						pausePlayer(getImages(' .playing'));
16618					},
16619					
16620				pausePlayer = function(el) {
16621						el.each(function() {
16622								var pl = $('#' + $(this).attr('rel'));
16623								
16624								if (pl.length) {
16625									pl[0].pause();
16626									$(this).removeClass('icon-pause playing').addClass('icon-volume-up paused');
16627								}
16628							});
16629					},
16630					
16631				/********************************************************
16632				 *
16633				 *						Rating
16634				 *
16635				 ********************************************************/
16636				
16637				// Reads ratings from local storage
16638				
16639				loadRatings = function(items) {
16640					
16641						if (typeof items === UNDEF || !items.length) {
16642							return;
16643						}
16644						
16645						var r = $.cookie(REL_PATH + PAGE_NAME + ':ratings');
16646						
16647						if (r) {
16648							if (typeof r === STRING) {
16649								r = r.split(':');
16650							}
16651							
16652							for (var i = 0; i < items.length && i < r.length; i++) {
16653								if (r[i] !== -1) {
16654									items[i][J.VISITORRATING] = r[i];
16655								}
16656							}
16657						}
16658						
16659						return items; 
16660					},
16661					
16662				// Saves visitor ratings to local storage
16663				
16664				saveRatings = function(items) {
16665						
16666						if (typeof items === UNDEF || !items.length) {
16667							return;
16668						}
16669						
16670						var r = [];
16671						
16672						for (var i = 0; i < items.length; i++) {
16673							r.push(items[i].hasOwnProperty(J.VISITORRATING)? items[i][J.VISITORRATING] : -1);
16674						}
16675						
16676						$.cookie(REL_PATH + PAGE_NAME + ':ratings', r, 604800);
16677					},
16678					
16679				/********************************************************
16680				 *
16681				 *					Image overlay rendering 
16682				 *			or initializing images in the base overlay
16683				 *
16684				 ********************************************************/
16685
16686				lazyloadTimeout 	= null,													// Lazy load timeout
16687				fixTimeout 			= null,													// Layout fixing timeout
16688				
16689				renderImages = function(overlay, items, complete) {
16690					
16691						if (!overlay || !items || !items.length) {
16692							// Invalid
16693							return;
16694						}
16695					
16696						if (!Array.isArray(items)) {
16697							// Single item
16698							items = [ items ];
16699						}
16700						
16701						appendProgressbar(DEBUG? text.rendering : '');
16702						
16703						var baseOly				= isBaseOverlay(),										// Base overlay? 
16704							ns 					= getOverlayNS(),										// Namespace
16705							imageContainer		= getImageContainer(true),								// image container (for overlays)
16706							cards 				= getCards(),											// Cards that already exist 
16707							index				= 0,													// Batch start
16708							itemCnt 			= items.length,											// Total count
16709							shopCnt 			= 0,													// Shoppable count
16710							cd 					= settings.hasOwnProperty('markNew')?					// Current day 
16711													(nows - settings.markNew.days * ONEDAY_S) 
16712													: 
16713													null,
16714							
16715							// Initializing one batch of images
16716							
16717							initImages = function() {
16718									
16719									if (index >= itemCnt) {
16720										// Finished
16721										finishInit();
16722										return;
16723									}
16724									
16725									var	max = Math.min(index + settings.thumbsPerBatch, itemCnt),
16726										item,
16727										isFolder,
16728										isLbable,
16729										isMedia,
16730										isIcon,
16731										ar,
16732										s,
16733										card,
16734										image,
16735										img,
16736										hasImg,
16737										caption,
16738										d;
16739										
16740									for (var i = index; i < max; i++) {
16741										item = items[i];
16742										isFolder = item[J.CATEGORY] === 'folder';
16743										isLbable = !isFolder && album.isLightboxable(item);
16744										isMedia = item[J.CATEGORY] === 'video' || item[J.CATEGORY] === 'audio';
16745										isIcon = /res\/\w+\.png$/.test(item[J.THUMB][J.PATH]);
16746										s = item[J.THUMBCAPTION];
16747									
16748										if (i < cards.length) {
16749											// Already exists in the HTML source
16750											card = cards.eq(i);
16751										} else {
16752											// Not yet exists in the HTML source: happens only on overlays
16753											ar = item.hasOwnProperty(J.THUMB)? item[J.THUMB][J.WIDTH] / item[J.THUMB][J.HEIGHT] : 1;
16754											card = $('<div>', {
16755														'class': 	[ 
16756																	settings.cardClass, 
16757																	settings.lazyloadClass, 
16758																	item[J.CATEGORY],
16759																	s? 'hascaption' : '',
16760																	(item.hasOwnProperty(J.THUMB) && item[J.THUMB][J.WIDTH] < settings.thumbDims[0] && item[J.THUMB][J.HEIGHT] < settings.thumbDims[1])? 'tiny' : '',
16761																	isIcon? 'icon' : '',
16762																	(ar >= 1.25)? 'landscape' : ((ar <= 0.8)? 'portrait' : 'square')
16763																].filter(Boolean).join(' ')
16764															})
16765														.appendTo(imageContainer);
16766										}
16767										
16768										// Lightboxable
16769										if (isLbable) {
16770											card.addClass(settings.lbableClass);
16771										}
16772										
16773										// Data pointer
16774										card.data(J.OBJ, item);
16775										
16776										// Finding/creating image's <a class="thumb"> wrap
16777										image = card.children('.' + settings.thumbClass);
16778										
16779										if (!image.length) {
16780											image = $('<a>', {
16781													'class':	settings.thumbClass
16782												})
16783												.appendTo(card);
16784										}
16785										
16786										// Finding/creating image
16787										img = image.find('img').eq(0);
16788										
16789										if (!img.length) {
16790											// Creating placeholder DIV
16791											img = $('<div>', {
16792													'class':	settings.placeholderClass
16793												}).appendTo(image);
16794											hasImg = false;
16795										} else {
16796											hasImg = true;
16797										}
16798										
16799										if (isIcon) {
16800											// Icon
16801											image.addClass('iconthumb');
16802										} else {
16803											
16804											if (item[J.THUMB].hasOwnProperty(J.RENDITIONS) && settings.thumbLayout !== 'fixgrid' && settings.thumbLayout !== 'grid') {
16805												// Has renditions, wait if larger image is needed
16806												img.data('hasrenditions', true);
16807											}
16808											
16809											if (!isMedia) {
16810												// Adding width, height
16811												if (hasImg) {
16812													img.attr({
16813															width:		item[J.THUMB][J.WIDTH],
16814															height:		item[J.THUMB][J.HEIGHT]
16815														});
16816												} else {
16817													img.css({
16818															width:		item[J.THUMB][J.WIDTH],
16819															height:		item[J.THUMB][J.HEIGHT]
16820														});
16821												}
16822												
16823												// Adding pano class
16824												if ((item[J.THUMB][J.HEIGHT] / item[J.THUMB][J.WIDTH]) < 0.5) {
16825													card.addClass(settings.panoClass);
16826												}
16827											}
16828										}
16829										
16830										if (settings.thumbLayout === 'justified') {
16831											// Justified: Saving width as data for later
16832											card.data('width', item[J.THUMB][J.WIDTH]);
16833										}
16834										
16835										// Save image name: for later
16836										if (!image.data('name')) {
16837											image.attr('data-name', album.getItemName(item));
16838										}
16839									
16840										// Caption
16841										if (s) {
16842			
16843											caption = card.children('.' + settings.captionClass);
16844											
16845											if (!caption.length) {
16846												// No caption yet
16847												caption = $('<div>', {
16848														'class': 		settings.captionClass,
16849														html: 			s
16850													}).appendTo(card);
16851													
16852												if (settings.thumbLayout === 'justified') {
16853													caption.css('maxWidth', item[J.THUMB][J.WIDTH]);
16854												}
16855											}
16856											
16857											card.addClass(settings.hascaptionClass);
16858										}
16859										
16860										// Preparing for Lightbox
16861										if (isLbable) {
16862												
16863											// Click event
16864											image.on('click.' + ns, imageClicked);
16865											
16866											// Saving href
16867											if (!image.data('href')) {
16868												image.attr('data-href', image.attr('href') || album.getOptimalImagePath(item, [ image[0].offsetWidth, image[0].offsetHeight ]));
16869											}
16870											
16871											// Removing link (to avoid unwanted navigation)
16872											// Links are included to help robots finding the slide images
16873											if (image[0].hasAttribute('href')) {
16874												image.attr('href', '');
16875											}
16876											
16877											var hasShop = shopRoot.length && album.hasShop(item),
16878												hasFeedback = feedbackRoot.length && !album.disableFeedback(item);
16879											
16880											// Adding select box
16881											if (hasShop || hasFeedback) {
16882												card.addClass(settings.selectableClass);
16883											}
16884											
16885											// Adding shop button
16886											if (hasShop) {
16887												d = $('<a>', {
16888															'class': 	'icon-shopping-cart add-cart',
16889															text: 		' ' + text.addCart
16890														})
16891													.on('click.' + ns, function(e) {
16892															var card = $(e.target).closest('.' + settings.cardClass);
16893															if (card.length) {
16894																shopRoot.trigger('addItems', card.data(J.OBJ));
16895															}
16896															return false;
16897														})
16898													.appendTo(image);
16899													
16900												d.addTooltip(album.getPriceRange(item), { delay: 100 });
16901												
16902												shopCnt += 1;
16903												
16904												card.addClass(settings.shoppableClass);
16905											}
16906											
16907											// Adding feedback button
16908											if (!hasShop && hasFeedback) {
16909												d = $('<a>', {
16910															'class': 	'icon-email-send add-cart',
16911															text: 		' ' + settings.feedback.addBtnLabel
16912														})
16913													.on('click.' + ns, function(e) {
16914															var card = $(e.target).closest('.' + settings.cardClass);
16915															if (card.length) {
16916																feedbackRoot.trigger('addItems', card.data(J.OBJ));
16917															}
16918															return false;
16919														})
16920													.appendTo(image);
16921											}
16922											
16923											// Adding Fotomoto button (if no shop button)
16924											if ((!LOCAL || DEBUG) && !hasShop &&
16925												settings.hasOwnProperty('fotomoto') && !album.hideFotomoto(item)) {
16926											
16927												d = $('<a>', {
16928															'class': 	'icon-shopping-cart add-cart fotomoto-btn',
16929															text: 		' ' + text.fotomotoBtn
16930														})
16931													.on('click.' + ns, function(e) {
16932															e.preventDefault();
16933															if (typeof FOTOMOTO !== UNDEF) {
16934																var card = $(e.target).closest('.' + settings.cardClass);
16935																if (card.length) {
16936																	FOTOMOTO.API.showWindow(10, 
16937																			album.getOriginalPath(card.data(J.OBJ)) || album.getItemPath(card.data(J.OBJ))
16938																		);
16939																}
16940															}
16941															return false;
16942														})
16943													.appendTo(image);
16944													
16945												d.addTooltip(text.fotomotoTooltip);
16946											}
16947											
16948											if (!baseOly && settings.linkToFolder) {
16949												// Folder link
16950												var p = album.getPath(item);
16951												
16952												if (p) {
16953													image.append($('<a>', {
16954															'class': 	'icon-folder folder-link',
16955															href:		 p + settings.indexName
16956														}));
16957												}
16958											}
16959											
16960											// Adding audio clip button
16961											if (settings.useAudioClipButton && item.hasOwnProperty(J.AUDIOCLIP)) {
16962												s = $('<a>', {
16963															'class':	'icon-volume-up audioclip',
16964															href:		item[J.AUDIOCLIP][J.PATH]
16965														}).on('click', togglePlayer)
16966														.appendTo(image);
16967														
16968												if (!TOUCHENABLED) {
16969													s.data('tooltip', text.audioClipLabel).addTooltip();
16970												}
16971											}
16972										} else {
16973											// Not lightboxable, e.g. webLocation or folder: using the link
16974											card.addClass(item[J.CATEGORY]);
16975											if (!image[0].href) {
16976												image.attr('href', album.getItemPath(item));
16977											}
16978										}
16979												
16980										// Mark new images
16981										if (cd != null) {
16982											
16983											d = item[J.DATES];
16984											
16985											if (d && (d = d[settings.markNew.reference]) && d >= cd) {
16986												s = settings.markNew.text?
16987													$('<span>', {
16988															'class':	'new-image',
16989															text:		settings.markNew.text
16990														})
16991													:
16992													$('<span>', {
16993														'class':	'icon-new-fill new-image'
16994														});
16995												
16996												image.append(s);
16997													
16998												if (!TOUCHENABLED) {
16999													s.data('tooltip', (new Date(d * 1000)).toLocaleDateString(LOCALE)).addTooltip({ delay: 100 });
17000												}
17001											}
17002										}
17003										
17004										// Rating
17005										if (settings.useRating && isLbable) {
17006											
17007											var r = item[J.RATING] || 0;
17008											
17009											if (settings.visitorRating || r) {
17010												
17011												s = $('<span>', {
17012														'class':	'rating',
17013														html:		STARS
17014													})
17015													.appendTo(image);
17016													
17017												if (settings.visitorRating) {
17018													
17019													if (item.hasOwnProperty(J.VISITORRATING)) {
17020														// Already exists
17021														r = item[J.VISITORRATING];
17022													} else {
17023														if (!settings.jalbumRating) {
17024															// Ignore jAlbum rating
17025															r = 0;
17026														}
17027														// Save the default
17028														item[J.VISITORRATING] = r;
17029													}
17030													
17031													s.addClass('r' + r + (settings.jalbumRating? '' : ' icon-close'))
17032														.on('click.' + ns, function(e) {
17033																var el = $(this),
17034																	r = Math.minMax(0, Math.floor((getCoords(e).x - el.offset().left - parseFloat(el.css('paddingLeft'))) / 16), 5),
17035																	item = el.closest('.' + settings.cardClass).data(J.OBJ),
17036																	extDir = false;
17037																	//Math.minMax(0, Math.floor((getCoords(e).x - el.offset().left + 4) / el.width()) * 5, 5);
17038																	
17039																el.removeClass('r0 r1 r2 r3 r4 r5').addClass('r' + r);
17040																item[J.VISITORRATING] = r;
17041																
17042																if (!baseOly) {
17043																	// Propagate to base overlay
17044																	el = getCardByName(item[J.NAME], true);
17045																	if (el.length) {
17046																		el.find('.rating')
17047																			.removeClass('r0 r1 r2 r3 r4 r5')
17048																			.addClass('r' + r);
17049																		el.data(J.OBJ)[J.VISITORRATING] = r;
17050																		saveRatings(getBaseItems());
17051																	}
17052																} else {
17053																	saveRatings(items);
17054																}
17055																
17056																return false;
17057															});
17058												} else {
17059													// Unalterable rating from jAlbum
17060													s.addClass('r' + r);
17061												}
17062											}
17063										}
17064										
17065										// Prevent copy
17066										if (settings.rightClickProtect) {
17067											image.on('contextmenu', function(e) {
17068													e.preventDefault();
17069													return false;
17070												});
17071										}
17072										
17073										if (!baseOly) {
17074											setProgressbar(i / itemCnt);
17075										}
17076										
17077									}
17078									
17079									index = max;
17080									window.requestAnimationFrame(initImages);
17081								},
17082								
17083							// Finishing initialization
17084								
17085							finishInit = function() {
17086								
17087									removeProgressbar();
17088									
17089									main.toggleClass(settings.hasShopClass, shopCnt > 0);
17090									main.toggleClass(settings.noShopClass, !shopCnt);
17091									
17092									overlay.on({
17093											setactivecard:		function(e, card) {
17094																		// Moving the active element into view
17095																		setActiveCard($(card));
17096																	},
17097									
17098											refreshRating:		function(e, card) {
17099																		// Refreshing the rating on the thumbnails
17100																		refreshRating($(card));
17101																	},
17102											
17103											refresh:			function(e) {
17104																		fixLayout();
17105																	}
17106										});
17107									
17108									//console.log('renderImages took ' + Math.round((new Date() - timer) / 1000) + 'ms');
17109									// Done function
17110									if (typeof complete === FUNCTION) {
17111										if (baseOly) {
17112											complete.call(undefined, overlay, baseOly);
17113										} else {
17114											complete.call(undefined, overlay);
17115										}
17116									}
17117									
17118									// Fixing layout only on screen size change
17119									$window.on('resize.' + ns, function(e) {
17120											clearTimeout(fixTimeout);
17121											fixTimeout = setTimeout(fixLayout, 500);
17122											return true;
17123										});
17124									
17125									fixLayout();
17126								},
17127						
17128							// Justifying rows
17129														
17130							justifyRows = function(doneFn) {
17131									
17132									clearTimeout(fixTimeout);
17133									
17134									// No thumbnails section?
17135									if (!overlay.length) {
17136										return;
17137									}
17138									
17139									// Visible?
17140									if (!overlay.is(':visible')) {
17141										// Retry until imagecontainer gets visible
17142										fixTimeout = setTimeout(justifyRows, 100);
17143										return;
17144									}
17145									
17146									var	cw = overlay.children('.' + settings.contClass).width(),
17147										mcw = 'calc(100% - ' + (2 * settings.thumbGap + 10) + 'px)';
17148									
17149									if (overlay.innerWidth() < settings.pageWidth && overlay.hasClass('overlay')) {
17150										// Fixing fantom scrollbars
17151										cw += 32;
17152									}
17153									
17154									// Is it already fixed for this width?
17155									if (overlay.data('fixedfor') === cw) {
17156										if (typeof doneFn === FUNCTION) {
17157											doneFn.call();
17158										}
17159										return;
17160									}
17161									
17162										// Justifying rows									
17163									var	justifyRow = function(cards, f) {
17164												
17165												if (f > 1.6) {
17166													f = 1.6;
17167												}
17168												
17169												var h = Math.floor(settings.thumbDims[1] * f);
17170												
17171												cards.each(function() {
17172														var el = this.querySelector('.' + settings.thumbClass);
17173														if (el) {
17174															if (f === 1) {
17175																this.style.width = '';
17176																el.style.height = h + 'px';
17177															} else {
17178																this.style.width = $(this).data('width') * f + 'px';
17179																el.style.height = h + 'px';
17180																el = this.querySelector('.' + settings.captionClass);
17181																if (el) {
17182																	el.style.removeProperty("max-width");
17183																}
17184															}
17185														}
17186													});
17187											},
17188											
17189										justifySection = function(cards) {
17190												
17191												if (!cards.length) {
17192													return;
17193												}
17194												
17195												var i = 0, 
17196													rw = 0,
17197													w;
17198												
17199												while (cards.length && i < cards.length) {
17200													
17201													w = cards.eq(i).data('width') + 2 * settings.thumbGap;
17202													
17203													if ((rw + w) >= cw) {
17204														// Overflown
17205														if (rw + (w * settings.justifyWrapThreshold) >= cw) {
17206															// Wrap this item
17207															justifyRow(cards.slice(0, i), (cw - 2 * i * settings.thumbGap) * .9999 / (rw - 2 * i * settings.thumbGap));
17208															cards = cards.slice(i);
17209															
17210														} else {
17211															// With this item
17212															justifyRow(cards.slice(0, i + 1), (cw - 2 * (i + 1) * settings.thumbGap) * .9999 / (rw + w - 2 * (i + 1) * settings.thumbGap));
17213															cards = cards.slice(i + 1);
17214														}
17215														
17216														i = 0;
17217														rw = 0;
17218														
17219													} else {
17220														
17221														i++;
17222														rw += w;
17223													}
17224												} 
17225												
17226												if (cards.length > 1) {
17227													// Remainder: the last row(s)
17228													var f = (cw - 2 * cards.length * settings.thumbGap) * .9999 / (rw - 2 * cards.length * settings.thumbGap);
17229													// Max. 20% zoom
17230													justifyRow(cards, (f > 1.2)? 1 : f);
17231												} else {
17232													// Single image
17233													justifyRow(cards, 1);
17234												}
17235												
17236												// Remembering last fixed width
17237												overlay.data('fixedfor', cw);
17238											};
17239									
17240									// Starts here
17241									
17242									justifySection(overlay.find('.'  + settings.cardClass));
17243									
17244									if (typeof doneFn === FUNCTION) {
17245										doneFn.call();
17246									}
17247									
17248								},
17249																										
17250							// Fixing layout for vertical and justified layouts and lazy loading thumbs
17251							
17252							fixLayout = function() {
17253								
17254									clearTimeout(fixTimeout);
17255									
17256									// Fixing layout, starting lazyload on complete
17257									if (settings.thumbLayout === 'justified') {
17258										fixTimeout = setTimeout(justifyRows, 100, lazyloadThumbs);
17259									} else {
17260										fixTimeout = setTimeout(lazyloadThumbs, settings.scrollCheckInterval / 2);
17261									}
17262								},
17263											
17264							// Clicked image
17265										
17266							imageClicked = function(e) {
17267									if (e.target.nodeName === 'A' && ($(e.target).closest('.' + settings.commentClass).length || $(e.target).hasClass('folder-link'))) {
17268										// Link in comment or link to folder
17269										return true;
17270									}
17271									
17272									$body.hideAllTooltips();
17273									//console.log('(SMALLSCREEN === settings.lightboxFullscreenOnlyMobiles) = (' + SMALLSCREEN + '===' + settings.lightboxFullscreenOnlyMobiles + ') =' + (SMALLSCREEN === settings.lightboxFullscreenOnlyMobiles));
17274									if (!isFullscreen() && settings.lightboxFullscreen && (SMALLSCREEN === settings.lightboxFullscreenOnlyMobiles)) {
17275										requestFullscreen();
17276									}
17277									lightboxLoad($(this).closest('.' + settings.cardClass));
17278									
17279									return false;
17280								},
17281								
17282							// Marking active card
17283						
17284							setActiveCard = function(card) {
17285								
17286									if (typeof card === UNDEF || card instanceof Event || !card.length) {
17287										return;
17288									}
17289									
17290									var item 		= card.data(J.OBJ),
17291										wh 			= $window.height(),
17292										scrollTop 	= $window.scrollTop(),
17293										ct 			= card.offset().top,
17294										ch 			= card.outerHeight();
17295									
17296									getCards('.' + settings.activeClass).removeClass(settings.activeClass);
17297									card.addClass(settings.activeClass);
17298									
17299									// Triggering active placemark
17300									if (settings.hasOwnProperty('map')) {
17301										if (album.hasLocation(item)) {
17302											getMapRoot().trigger('setActive', item[J.NAME]);
17303										} else {
17304											getMapRoot().trigger('resetMarkers');
17305										}
17306									}
17307									
17308									// Moving into view
17309									if (ct + ch > scrollTop + wh) {
17310										window.scrollTo({ 
17311											top: 		Math.round(ct - wh + ch + 30), 
17312											behavior: 	'smooth'
17313										});
17314									} else if (ct < scrollTop) {
17315										window.scrollTo({ 
17316											top: 		Math.round(Math.max(ct - 30, 0)), 
17317											behavior: 	'smooth'
17318										});
17319									}
17320								},
17321					
17322							// Refreshing the rating
17323							
17324							refreshRating = function(card, rating) {
17325								
17326									if (typeof card === UNDEF || !card.length) {
17327										return;
17328									}
17329									
17330									var o = card.data(J.OBJ),
17331										r = ((typeof rating !== UNDEF)? 
17332												rating 
17333												: 
17334												(settings.visitorRating? o[J.VISITORRATING] : o[J.RATING])
17335											) || 0;
17336										
17337									card.find('.rating')
17338										.removeClass('r0 r1 r2 r3 r4 r5')
17339										.addClass('r' + r);
17340										
17341									if (!isBaseOverlay()) {
17342										getCardByName(o[J.NAME], true).find('.rating')
17343											.removeClass('r0 r1 r2 r3 r4 r5')
17344											.addClass('r' + r);
17345									} else {
17346										saveRatings(items);
17347									}
17348								};
17349		
17350						// Creating new Unique ID if not yet exists
17351						if (!ns) {
17352							ns = setOverlayNS();
17353						}
17354						
17355						if (baseOly && settings.useRating && settings.visitorRating) {
17356							loadRatings(items);
17357						}
17358						
17359						window.requestAnimationFrame(initImages);
17360												
17361					},
17362					
17363				/********************************************************
17364				 *
17365				 *					Lazy loading
17366				 *
17367				 ********************************************************/
17368										
17369				// Loading visible thumbs
17370				
17371				lazyloadThumbs = function() {
17372					
17373						clearTimeout(lazyloadTimeout);
17374						lazyloadTimeout = null;
17375						
17376						if (lightboxOn()) {
17377							lazyloadTimeout = setTimeout(lazyloadThumbs, 100);
17378							return;
17379						}
17380						
17381						var st = $window.scrollTop(),
17382							wh = $window.height(),
17383							oly = getOverlay(),
17384							baseOly = isBaseOverlay(),
17385							cards;
17386						
17387						if (Math.abs(st - scrollTop) > 20) {
17388							// Scrolling in progress
17389							scrollTop = st;
17390							lazyloadTimeout = setTimeout(lazyloadThumbs, settings.scrollCheckInterval);
17391							return;
17392						}
17393						
17394						if (oly.length) {
17395							cards = oly.find('.' + settings.cardClass).not(':empty');
17396						} else {
17397							cards = main.find('.' + settings.foldersClass + ' .' + settings.cardClass).not(':empty');
17398						}
17399						
17400						// Filtering the images to be loaded
17401						cards = cards.filter(function() {
17402								if ($(this).hasClass(settings.lazyloadClass)) {
17403									// Not loaded yet
17404									return true;
17405								} else if ($(this).hasClass(settings.preloadClass)) {
17406									// Preloadedd thumbs
17407									var thumb = $(this).find('.' + settings.thumbClass), 
17408										img = thumb.find('img').eq(0);
17409									
17410									$(this).removeClass(settings.preloadClass);
17411									// Keep only those that have too small resolution
17412									return !settings.fixedShapeThumbs && img.data('hasrenditions') && 
17413										(img[0].width * 1.1 < thumb.width() || img[0].height * 1.1 < thumb.height());
17414								}
17415								// Already loaded
17416								return false;
17417							});
17418						
17419						if (cards.length) {
17420							
17421							cards.each(function() {
17422									var card = $(this),
17423										ct = card.offset().top,
17424										ch = card.height();
17425										
17426									if ((ct + ch) > (scrollTop - wh)) {
17427										if (ct < (scrollTop + 1.5 * wh)) {
17428											// 2 screens ahead
17429											if (baseOly && card.hasClass(settings.folderClass)) {
17430												loadFolderThumb(card);
17431											} else {
17432												loadThumb(card);
17433											}
17434										} else {
17435											return false;
17436										}
17437									}
17438								});
17439						
17440							// perpetrating lazyload
17441							scrollTop = st;
17442							lazyloadTimeout = setTimeout(lazyloadThumbs, settings.scrollCheckInterval);
17443							
17444						} else {
17445							// No more left
17446							$window.off('scroll.' + ns);
17447						}
17448					},
17449		
17450				// Loading one folder
17451									
17452				loadFolderThumb = function(card) {
17453						
17454						if (typeof card === UNDEF || !card.length) {
17455							return;
17456						}
17457						
17458						var onloadFn = function() {
17459									var img = $(this);
17460									
17461									img.css({
17462											display:	'block',
17463											opacity:	1
17464										});
17465								};
17466						
17467						card.find('.thumb[data-src]').each(function() {
17468								var thumb = $(this),
17469									img = thumb.find('img:not(.mosimg)').eq(0);
17470							
17471								if (!img.length) {
17472									img = $('<img>', {
17473											alt: 			thumb.data('alt') || ''
17474										}).css({
17475											opacity:		0
17476										}).appendTo(thumb);
17477								}
17478								
17479								if (typeof img.attr('src') !== UNDEF) {
17480									onloadFn.call(img[0]);
17481								} else {
17482									img.one('load', onloadFn);
17483									img.attr('src', thumb.data('src'));
17484									thumb.removeData('src');
17485								}
17486								
17487							});
17488								
17489						card.find('.mosaic [data-src]').each(function() {
17490								var m = $(this),
17491									mi = m.children('img').eq(0);
17492									
17493								if (!mi.length) {
17494									mi = $('<img>').css({
17495											opacity:		0
17496										}).appendTo(m);
17497								}
17498								
17499								mi.one('load', onloadFn);
17500								mi.attr('src', m.data('src'));
17501								m.removeData('src');
17502							});
17503						
17504						card.removeClass(settings.lazyloadClass);
17505					},
17506					
17507				// Loading one thumb
17508				
17509				loadThumb = function(card) {
17510						
17511						if (typeof card === UNDEF || !card.length) {
17512							return;
17513						}
17514						
17515						var item = card.data(J.OBJ);
17516						
17517						if (!item) {
17518							return;
17519						}
17520						
17521						var	thumb = card.children('.' + settings.thumbClass).eq(0),
17522							img = thumb.find('img'),
17523							onloadFn = function() {
17524									var img = $(this),
17525										card = img.parents('.' + settings.cardClass).eq(0),
17526										ph = card.find('.' + settings.placeholderClass);
17527									
17528									//window.requestAnimationFrame(function() {
17529									if (ph.length) {
17530										ph.remove();
17531									}
17532									
17533									img.css({
17534											display: 		'block',
17535											opacity:		1
17536										});
17537											
17538									//	});
17539									
17540									card.removeClass(settings.lazyloadClass + ' ' + settings.preloadClass);
17541								};
17542						
17543						if (!img.length) {
17544							// Create IMG element if missing
17545							img = $('<img>', {
17546									alt:			album.getAlt(item)
17547								}).css({
17548									aspectRatio:	item[J.THUMB][J.WIDTH] + '/' + item[J.THUMB][J.HEIGHT],
17549									display: 		'none',
17550									opacity:		0
17551								}).appendTo(thumb);
17552						}
17553						
17554						if (card.hasClass(settings.lazyloadClass) && typeof img.attr('src') !== UNDEF && img.attr('src') !== false) {
17555							// IMG already has source and lazyloaded
17556							onloadFn.call(img[0]);
17557						} else {
17558							// Missing or low res.
17559							img.one('load', onloadFn);
17560							// Set source: optimal thumbnail if fixedShape, can fall back to images otherwise  
17561							img.attr('src', img.data('hasrenditions')?
17562								album.getOptimalThumbPath(card.data(J.OBJ), [ thumb.outerWidth(), thumb.outerHeight() ], !settings.fixedShapeThumbs)
17563								:
17564								album.getThumbPath(item));
17565						}
17566					},
17567								
17568				/********************************************************
17569				 *
17570				 *					Preparing images
17571				 *
17572				 ********************************************************/
17573					
17574				prepareImages = function(overlay, baseOly) {
17575					
17576						if (!album) {
17577							return;
17578						}
17579						
17580						if (typeof overlay === UNDEF) {
17581							overlay = getBaseOverlay();
17582							baseOly = true;
17583						} else if (typeof baseOly === UNDEF) {
17584							baseOly = isBaseOverlay(overlay);
17585						}
17586							
17587						var cards = getCards(),
17588							imgs = cards.filter('.' + settings.lbableClass);
17589							
17590						// Start lazy loading and layout corrections
17591						overlay.trigger('refresh');
17592							
17593						if (imgs.length > 1 || settings.showEmptyFilters) {
17594							// Filtering and sort only if there are at least 2 items
17595							if ($('.filter-cont').length) {
17596								// Initializing Filters / Sort
17597								prepareFilterBox(baseOly);
17598							}
17599						}
17600						
17601						if (imgs.length) {
17602							// Has lighboxable images
17603							
17604							// Selection boxes
17605							if (album.hasShop() || settings.feedback) {
17606								var ci = getSelectableCards();
17607								
17608								toggleSelectButtons(0, ci.length);
17609								toggleBoxes(ci.length);
17610								
17611								if (ci.length) {
17612									// Make items selectable
17613									overlay.selectable({
17614											cardClass:			settings.selectableClass,
17615											selectedClass:		settings.selectedClass,
17616											selectionChange:	function(n) {
17617																		toggleSelectButtons(n, ci.length);
17618																		shopRoot.add(feedbackRoot).trigger('itemsSelected', n);
17619																	}
17620										});
17621									
17622									// Keep selected items button wiring
17623									shopBox.add(feedbackBox)
17624										.find('.' + settings.keepSelectedClass).on('click.' + ns, function() {
17625											keepSelectedItems(text.selectedItems, function() {
17626													setHash();
17627												});
17628											
17629											return false;
17630										});
17631										
17632									// Initialized
17633									content.find('[data-sticky-rel]').trigger('stickyRefresh');
17634									// Uninited
17635									content.find('[data-sticky]').not('[data-sticky-rel]').sticky();
17636									
17637								}
17638							}
17639						
17640							// Initializing map
17641							if (settings.hasOwnProperty('map')) {
17642								// Pass settings to lightbox defaults
17643								if (settings.map.hasOwnProperty('apiKey')) {
17644									$.fn.lightbox.defaults['mapApiKey'] = settings.map.apiKey;
17645								}
17646								if (settings.map.hasOwnProperty('type')) {
17647									$.fn.lightbox.defaults['mapType'] = settings.map.type;
17648								}
17649								if (settings.map.hasOwnProperty('zoom')) {
17650									$.fn.lightbox.defaults['mapZoom'] = settings.map.zoom;
17651								}
17652								if (settings.map['index']) {
17653									prepareMap(getAllItems(), settings.map);
17654								}
17655							}
17656							
17657							/* Waiting for Derek's answer
17658							if (!LOCAL && typeof FOTOMOTO !== UNDEF && settings.fotomoto) {
17659								FOTOMOTO.API.checkinImages(getFotomotoImages(cards), function(images) {
17660										
17661									});
17662							}
17663							*/
17664							
17665							// Removing the lightbox, athough this should not happen
17666							overlay.trigger('lightboxRemove');
17667							
17668							overlay.lightbox(album, {
17669									
17670									baseSet:		baseOly,
17671									
17672									onLoadStart:	function(card) {
17673															var item = card.data(J.OBJ);
17674															
17675															if (item) {
17676																addParam(settings.pageName || settings.indexName, {
17677																		img: 		album.getItemName(item)
17678																	}, item[J.TITLE]);
17679															}						
17680														},
17681														
17682									onLoadEnd: 		function(card) {
17683															if (typeof card !== UNDEF) {
17684																overlay.trigger('setactivecard', card);
17685															}
17686															setTimeout(updateShares, 100);
17687														},
17688													
17689									onClose:		function(card) {
17690															if (isFullscreen() && settings.slideshowFullscreen && (SMALLSCREEN === settings.slideshowFullscreenOnlyMobiles)) {
17691																// Exit fullscreen only on desktops
17692																exitFullscreen();
17693															}
17694															if (typeof card !== UNDEF) {
17695																overlay.trigger('setactivecard', card);
17696															}
17697															updateShares();
17698															overlay.trigger('refresh');
17699															setHash();
17700														},
17701														
17702									onVolumeChange:	function(v) {
17703															settings.audioClipVolume = v;
17704														},
17705													
17706									onReady:		function() {
17707															if (initByHash && window.location.hash) {
17708																initByHash = false;
17709																var param = readParam();
17710																
17711																hashChangedByJs = true;
17712																
17713																if (param.hasOwnProperty('label') && param.label !== null) {
17714																	// Label filter
17715																	getItemsByLabel(param.label, function() {
17716																		addParam(settings.indexName, {
17717																				label:			param.label + '',
17718																				search:			null,
17719																				newimages:		null,
17720																				date:			null
17721																			}, text.label + ': ' + param.label);
17722																	});
17723																} else if (param.hasOwnProperty('search') && param.search !== null) {
17724																	// Search
17725																	getItemsBySearch(param.search, function() {
17726																		addParam(settings.indexName, {
17727																				search:			param.search + '',
17728																				label:			null,
17729																				newimages: 		null,
17730																				date:			null
17731																			}, text.search + ': ' + param.search);
17732																	});
17733																} else if (param.hasOwnProperty('newimages')) {
17734																	// Search new images
17735																	getItemsByDate(getSearchNewOptions(param.newimages), function() {
17736																		addParam(settings.indexName, {
17737																				newimages:		param.newimages,
17738																				search:			null,
17739																				label:			null,
17740																				date:			null
17741																			}, text.searchNew + ': ' + param.newimages);
17742																	});
17743																} else if (param.hasOwnProperty('date')) {
17744																	var options = getDateRangeOptions(param.date);
17745																	// Search images in date range
17746																	getItemsByDate(options, function() {
17747																		addParam(settings.indexName, {
17748																				date:			param.date,
17749																				newimages:		null,
17750																				search:			null,
17751																				label:			null
17752																			}, getDateReference(options.reference) + ': ' + param.date);
17753																	});
17754																} else if (param.hasOwnProperty('img') && param.img !== null) {
17755																	var card = getCardByName(param.img);
17756																	
17757																	if (card.length) {
17758																		if (param.hasOwnProperty('slideshow')) {
17759																			lightboxLoad(card, 2000);
17760																		} else {
17761																			lightboxLoad(card);
17762																		}
17763																	} else {
17764																		removeParam(settings.pageName || settings.indexName, 'img');
17765																	}
17766																} else if (param.hasOwnProperty('slideshow')) {
17767																	lightboxLoad(0, 2000);
17768																}
17769															}
17770															
17771															$('.' + settings.startshowClass).fadeIn().on('click', function() {
17772																	$(this).trigger('removeTooltip');
17773																	startSlideshow();
17774																	return false;
17775																});
17776														}
17777								});
17778							
17779						} else {
17780							// No images: remove the start slideshow button
17781							$('.' + settings.startshowClass).fadeOut();
17782						}
17783					},
17784				
17785				// Sets the URL by JS
17786				
17787				setHash = function(params, title) {
17788						var hash = (typeof params === UNDEF)? '' : '#' + paramize(params);
17789					
17790						if (hash !== window.location.hash) {
17791							hashChangedByJs = true;
17792							if (params) {
17793								setParam(settings.pageName || settings.indexName, params, title || '');
17794							} else {
17795								removeParam(settings.pageName || settings.indexName);
17796							}
17797							setTimeout(function() {
17798									// reseting hashByJs
17799									hashChangedByJs = false;
17800								}, 1000);
17801						}
17802					},
17803				
17804				// Hash change listener :: when the browser's back / forward buttons were pressed
17805				
17806				stateChange = function() {
17807						if (window.location.hash === lastHash || hashChangedByJs) {
17808							// Same hash or changed by JS (not on external trigger)
17809							return;
17810						}
17811						
17812						lastHash = window.location.hash;
17813						hashChangedByJs = false;
17814						initByHash = false;
17815						
17816						var param = readParam();
17817						
17818						if (param.hasOwnProperty('label') && param.label !== null) {
17819							// Label filter
17820							getItemsByLabel(param.label, function() {
17821									updateShares();
17822								});
17823						} else if (param.hasOwnProperty('search') && param.search !== null) {
17824							// Search
17825							getItemsBySearch(param.search, function() {
17826									updateShares();
17827								});
17828						} else if (param.hasOwnProperty('newimages') && param.newimages !== null) {
17829							// New images
17830							getItemsByDate(getSearchNewOptions(param.newimages), function() {
17831									updateShares();	
17832								});
17833						} else if (param.hasOwnProperty('date') && param.date !== null) {
17834							// Date range
17835							var options = getDateRangeOptions(param.date);
17836							getItemsByDate(options, function() {
17837									updateShares();	
17838								});
17839						} else if (param.hasOwnProperty('img') && param.img !== null) {
17840							// Image (no search or label)
17841							var card = getCardByName(param.img);
17842							if (card.length) {
17843								if (param.hasOwnProperty('slideshow')) {
17844									lightboxLoad(card, 2000);
17845								} else {
17846									lightboxLoad(card);
17847								}
17848							} else {
17849								removeParam(settings.indexName, 'img');
17850							}
17851						} else if (param.hasOwnProperty('slideshow')) {
17852							lightboxLoad(0, 2000);
17853						} else {
17854							// Plain index page
17855							lightboxQuit();
17856							removeOverlay();
17857						}
17858						
17859					},
17860				
17861				// Starts slideshow
17862				
17863				startSlideshow = function(e) {
17864						var card = $();
17865						
17866						if (!settings.restartSlideshow) {
17867							card = getCards('.' + settings.activeClass);
17868						}
17869						
17870						if (!card.length) {
17871							// Fall back to first
17872							card = getCards().eq(0);
17873						}
17874					
17875						if (!isFullscreen() && settings.slideshowFullscreen && (SMALLSCREEN === settings.slideshowFullscreenOnlyMobiles)) { 
17876							requestFullscreen();
17877						}
17878						
17879						lightboxLoad(card, 2000);
17880						
17881						return false;
17882					},
17883					
17884				/********************************************************
17885				 *
17886				 *		Functionalities that results in new overlay
17887				 *
17888				 ********************************************************/
17889				
17890				// Preparing the overlay before the items arrive
17891				
17892				prepareOverlay = function(attr) {
17893						var oly = addOverlay(attr);
17894						
17895						appendProgressbar(DEBUG? text.loading : '');
17896						focusOverlay();
17897						
17898						return oly;
17899					},
17900					
17901				// Finishing the overlay when the items are ready
17902				
17903				renderOverlay = function(oly, items, ready) {
17904					
17905						removeProgressbar();
17906						renderImages(oly, items, prepareImages);
17907						
17908						toggleSelectButtons(0, items.length);
17909						toggleBoxes(items.length);
17910						toggleHasImages(items.length);
17911						
17912						if (typeof ready === FUNCTION) {
17913							ready.call();
17914						}
17915					},
17916					
17917				/********************************************************
17918				 *
17919				 *				Keeping only selected items
17920				 *
17921				 ********************************************************/
17922				
17923				keepSelectedItems = function(tx, ready) {
17924						var items = getSelectedItems();
17925						
17926						if (!items.length || items.length === getCards('.' + settings.lbableClass).length) {
17927							return;
17928						}
17929						
17930						var oly = prepareOverlay({
17931									className:	'selected-items',
17932									icon:		'icon-checkbox-checked',
17933									title:		'&ldquo;' + tx + '&rdquo;'
17934								});
17935							
17936						setTimeout(function() {
17937							
17938								appendToOverlayTitle($('<small>', {
17939										text: 		text.foundNTimes.replace('{0}', items.length)
17940									}));
17941								
17942								renderOverlay(oly, items, ready);
17943								
17944							}, 50);
17945					},
17946				
17947				/********************************************************
17948				 *
17949				 *					Search and tag search
17950				 *
17951				 ********************************************************/
17952				 
17953				// Executing Search
17954				
17955				getItemsBySearch = function(tx, ready) {
17956						var oly = prepareOverlay({
17957									className:	'search-results',
17958									icon:		'icon-search',
17959									title:		'&ldquo;' + tx + '&rdquo;'
17960								});
17961							
17962						album.collectItems($.extend({}, settings.search, {
17963								terms:			tx,
17964								depth:			'tree',
17965								ready:			function() {
17966														var found = this.length;
17967														
17968														if (this.length > settings.maxResults) {
17969															this.splice(settings.maxResults);
17970														}
17971															
17972														appendToOverlayTitle($('<small>', {
17973																text: 	(found > settings.maxResults)? 
17974																		text.foundTooMany.replace('{0}', found).replace('{1}', this.length)
17975																		:
17976																		found?
17977																		text.foundNTimes.replace('{0}', found) 
17978																		: 
17979																		text.notFound
17980															}));
17981														
17982														renderOverlay(oly, this, ready);
17983													}
17984							}));
17985					},
17986					
17987				// Preparing Search functionality
17988				
17989				prepareSearch = function(f) {
17990					
17991						if (!f.length || !album) {
17992							return;
17993						}
17994						
17995						// Adding defaults if missing
17996						
17997						settings.search = $.extend({
17998								depth:		'tree',
17999								fields:		'creator,keywords,title,comment'
18000							}, settings.search);
18001						
18002						var startSearch = function(f) {
18003								
18004								var tx = f.find('input').eq(0).val();
18005								
18006								if (tx && tx.length > 1) {
18007									getItemsBySearch(tx, function() {
18008											setHash({
18009													search:		tx
18010												}, text.search + ': ' + tx);
18011											updateShares();
18012										});
18013								}
18014								return false;
18015							};
18016							
18017						f.find('.search-btn').on('click', function() {
18018								var btn = $(this),
18019									form = btn.closest('form');
18020									
18021								if (form.hasClass('toggling')) {
18022									if (form.hasClass(settings.activeClass)) {
18023										form.removeClass(settings.activeClass);
18024										startSearch(form);
18025									} else {
18026										form.find('input').eq(0).focus();
18027										form.addClass(settings.activeClass);
18028									}
18029								} else {
18030									startSearch(form);
18031								}
18032								
18033								return false;
18034							});
18035						
18036						f.on('submit', function(e) {
18037								e.preventDefault();
18038								startSearch(f);
18039								f.removeClass(settings.activeClass);
18040								return false;
18041							});
18042						
18043						f.find('input').on('focus',	function() { 
18044								f.addClass(settings.activeClass); 
18045							});
18046					},
18047				
18048				/********************************************************
18049				 *
18050				 *						Tag cloud
18051				 *
18052				 ********************************************************/
18053				
18054				// Preparing Tag Search functionality
18055				
18056				prepareTagSearch = function(target) {
18057						var f = target.find('.' + settings.tagSearchClass),
18058							// Start tag search		
18059							startSearch = function(f) {
18060									var tx = f.find('input').eq(0).val();
18061									
18062									if (tx) {
18063										getItemsByLabel(tx, function() {
18064											setHash({
18065													label: 	tx
18066												}, text.label + ': ' + tx);
18067											updateShares();
18068										});
18069									}
18070									return false;
18071								};
18072						
18073						if (!f.length) {
18074							f = $('<form>', {
18075									'class': 		settings.tagSearchClass
18076								}).appendTo(target);
18077							
18078							f.append($('<div>', {
18079									'class':		'search-input'
18080								})
18081								.append($('<input>', {
18082									'type':			'text',
18083									placeholder:	text.searchPlaceholder,
18084									ariaLabel:		text.search
18085								})));
18086								
18087							f.append($('<a>', {
18088									'class':		'search-btn icon-search'
18089								}));
18090						}
18091						
18092						f.find('.search-btn').on('click', function() {
18093								var f = $(this).closest('form');
18094								f.find('input').eq(0).focus();
18095								startSearch(f);
18096								return false;
18097							});
18098						
18099						f.on('submit', function(e) {
18100								e.preventDefault();
18101								startSearch($(this));
18102								return false;
18103							});
18104						
18105					},
18106				
18107				// Search labels
18108			
18109				getItemsByLabel = function(tx, caseSensitive, ready) {
18110						var oly = prepareOverlay({
18111									className:		'tags-found',
18112									icon:			'icon-tag',
18113									title:			'&ldquo;' + tx + '&rdquo;'
18114								});
18115							
18116						if (typeof caseSensitive === UNDEF) {
18117							caseSensitive = false;
18118						} else if (typeof caseSensitive === FUNCTION) {
18119							ready = caseSensitive;
18120							caseSensitive = false;
18121						}
18122						
18123						album.collectItems($.extend({}, settings.tagCloud, {
18124								caseSensitive:	caseSensitive,
18125								exact:			true,
18126								terms:			tx,
18127								ready: 			function() {
18128														var found = this.length;
18129														
18130														if (this.length > settings.maxResults) {
18131															this.splice(settings.maxResults);
18132														}
18133														
18134														appendToOverlayTitle($('<small>', {
18135																text: 	(found > settings.maxResults)? 
18136																		text.foundTooMany.replace('{0}', found).replace('{1}', this.length)
18137																		:
18138																		found?
18139																		text.foundNTimes.replace('{0}', found) 
18140																		: 
18141																		text.notFound
18142															}));
18143														
18144														renderOverlay(oly, this, ready);
18145													}
18146							}));
18147					},
18148					
18149				// Collect items from a path array
18150				
18151				getItemsByPaths = function(tx, paths, ready) {
18152						var oly = prepareOverlay({
18153									className:	'tags-found',
18154									icon:		'icon-tag',
18155									title:		'&ldquo;' + tx + '&rdquo;'
18156								});
18157							
18158						album.collectByPath({
18159								paths:		paths,
18160								ready:		function() {
18161														var found = this.length;
18162														
18163														if (this.length > settings.maxResults) {
18164															this.splice(settings.maxResults);
18165														}
18166														
18167														appendToOverlayTitle($('<small>', {
18168																text: 	(found > settings.maxResults)? 
18169																		text.foundTooMany.replace('{0}', found).replace('{1}', this.length)
18170																		:
18171																		found?
18172																		text.foundNTimes.replace('{0}', found) 
18173																		: 
18174																		text.notFound
18175															}));
18176														
18177														renderOverlay(oly, this, ready);
18178													}
18179							});
18180					},
18181					
18182				// Add toggle button
18183				
18184				addToggleButton = function(t) {
18185						var	btn;
18186
18187						t.css({
18188								maxHeight: 	150,
18189								overflow: 	'hidden'
18190							});
18191						
18192						btn = $('<div>', {
18193								'class': 	'toggle-height'
18194							}).insertAfter(t);
18195						
18196						btn.append($('<a>', {
18197								'class': 	'icon-caret-down',
18198								text: 		text.more
18199							})
18200							.on('click', function() {
18201									var b = t.parent(),
18202										toggleHeight = function() {
18203												var h = t.prop('scrollHeight');
18204												
18205												b.css({
18206														height: 	'auto',
18207														overflow: 	'visible'
18208													});
18209												t.css({
18210														maxHeight: 	150,
18211													});
18212												setTimeout(function() {
18213														t.css({
18214																maxHeight: 	h + 20
18215															});
18216													}, 25);
18217												btn.addClass(settings.activeClass);
18218											};
18219									
18220									b.css({
18221											height: 	b.height(),
18222											overflow: 	'hidden'
18223										});
18224									
18225									t.css({
18226											maxHeight: 	'none'
18227										});
18228									
18229									setTimeout(toggleHeight, 50);
18230								
18231								}
18232						))
18233						.append($('<a>', {
18234								'class': 	'icon-caret-up',
18235								text: 		text.less
18236							}).on('click', function() {
18237								t.css({
18238										maxHeight: 	150
18239									});
18240								btn.removeClass(settings.activeClass);
18241							}));
18242					
18243					},
18244				
18245				// Collecting and displaying tags
18246			
18247				prepareTagCloud = function(tags) {
18248						var tc = $(settings.tagCloud['hook'] || '.tag-cloud-cont');
18249						
18250						if (!tc.length) {
18251							return;
18252						}
18253						
18254						if (tags.length) {
18255							var t = tc.children('.' + settings.tagCloudClass),
18256								// Click function
18257								tagClicked = function() {
18258										var tx = $(this).children('span').eq(0).text();
18259										
18260										if (tx) {
18261											getItemsByPaths(tx, $(this).data('paths'), function() {
18262												setHash({
18263															label: 		tx
18264														}, text.label + ': ' + tx);
18265													updateShares();
18266												});
18267										}
18268										
18269										return false;
18270									};
18271							
18272							// Creating wrap el if not exists
18273							if (!t.length) {
18274								t = $('<div>', {
18275										'class':		settings.tagCloudClass
18276									}).appendTo(tc);
18277							}
18278								
18279							// Adding tags
18280							for (var a, r, cnt, i = 0, l = tags.length; i < l; i++) {
18281								
18282								// Creating link element
18283								a = $('<a>', {
18284											'class': 	'tag'
18285										})
18286									.on('click', tagClicked)
18287									.data('paths', tags[i][1])
18288									.append($('<span>', {
18289											html: 		tags[i][0]
18290										}));
18291								
18292								// Adding count
18293								if (tags[i][1].length > 1) {
18294									
18295									if (settings.tagCloud.fontVaries) {
18296										r = 0;
18297										cnt = Math.min(64, tags[i][1].length);
18298										// calculate weight
18299										while (cnt > 1) {
18300											r++;
18301											cnt = cnt >> 1;
18302										}
18303										a.addClass('size-' + r);
18304									}
18305									
18306									a.append('&nbsp;').append($('<sup>', {
18307											text: 		tags[i][1].length
18308										}));
18309								}
18310								
18311								t.append(a);
18312							}
18313							
18314							// Adding toggle button if list is too high
18315							if (t.outerHeight() > 175) {
18316								addToggleButton(t);
18317							}
18318						}
18319						
18320						// Adding search box
18321						if (settings.tagCloud['useSearch']) {
18322							prepareTagSearch(tc.parent());
18323						} else if (!tags.length) {
18324							// Nothing found
18325							tc.parents('.box').eq(0).hide();//remove();
18326						}
18327						
18328					},
18329				
18330				/********************************************************
18331				 *
18332				 *						Search new images
18333				 *
18334				 ********************************************************/
18335				
18336				getSearchNewOptions = function(days) {
18337					
18338						return settings.hasOwnProperty('searchNew')?
18339							{
18340								reference:		settings.searchNew['reference'] || 'dateTaken',
18341								depth:			settings.searchNew['depth'] || 'subfolders',
18342								start:			nows / ONEDAY_S - days
18343							}
18344							:
18345							{
18346								reference:		'dateTaken',
18347								depth:			'subfolders',
18348								start:			nows / ONEDAY_S - days
18349							};
18350						
18351					},
18352				
18353				// Search for new images
18354				
18355				getDateRangeOptions = function(range) {
18356						var m = range.match(/^([amt])?([cts])?\:?(\d+)\-?(\d+)?/i),
18357							o = {
18358									range:		0,
18359									reference:	'dateTaken',
18360									depth:		'current'
18361								};
18362								
18363						if (m) {
18364							if (m[1]) {
18365								// Reference
18366								if (m[1] === 'a') {
18367									o.reference = 'added';
18368								} else if (m[1] === 'm') {
18369									o.reference = 'fileModified';
18370								} else if (m[1] === 't') {
18371									o.reference = 'dateTaken';
18372								}
18373							}
18374							
18375							if (m[2]) {
18376								// Depth
18377								if (m[2] === 'c') {
18378									o.depth = 'current';
18379								} else if (m[2] === 't') {
18380									o.depth = 'tree';
18381								} else if (m[2] === 's') {
18382									o.depth = 'subfolders';
18383								}
18384							}
18385							
18386							if (m[4]) {
18387								// Range
18388								o.start = parseInt(m[3]) || 0;
18389								o.end = parseInt(m[4]);
18390							} else if (m[3]) {
18391								// Single day
18392								o.start = parseInt(m[3]);
18393								o.range = 1;
18394							}
18395						}
18396						
18397						return o;
18398					},
18399				
18400				getDateReference = function(r) {
18401					
18402						if (r === 'dateTaken') { 
18403							return text.imagesTaken;
18404						} else if (r === 'fileModified') {
18405							return text.imagesModified;
18406						}
18407						
18408						return text.imagesAdded;
18409					},
18410					
18411				getDateRangeTitle = function(o) {
18412						var t1 = getDateReference(o['reference']),
18413							t2,
18414							date = function(d) {
18415									return new Date(d * ONEDAY_MS).toLocaleDateString(LOCALE);
18416								};
18417						
18418						if (!o.hasOwnProperty('end') && !o.hasOwnProperty('start')) {
18419							t2 = getTimespan(o.range);
18420						} else if (!o.hasOwnProperty('end')) {
18421							if (o.hasOwnProperty('start')) {
18422								if (o.hasOwnProperty('range')) {
18423									if (o['range'] > 1) {
18424										t2 = text.betweenDays.template(date(o.start), date(o.start + o.range));
18425									} else {
18426										t2 = text.onDay.template(date(o.start));
18427									}
18428								} else {
18429									t2 = getTimespan(nows / ONEDAY_S - o.start);
18430								}
18431							}
18432						} else {
18433							t2 = text.beforeDay.template(date(o.end));
18434						}	
18435						
18436						return t1 + ' ' + t2;
18437					},
18438				
18439				getItemsByDate = function(options, ready) {
18440						var oly = prepareOverlay({
18441									className:		'search-results',
18442									icon:			'icon-calendar',
18443									title:			getDateRangeTitle(options)
18444								});
18445						
18446						album.collectByDate($.extend({}, options, {
18447									ready:		function() {
18448														var found = this.length;
18449														
18450														if (this.length > settings.maxResults) {
18451															this.splice(settings.maxResults);
18452														}
18453														
18454														appendToOverlayTitle($('<small>', {
18455																text: 	(found > settings.maxResults)? 
18456																		text.foundTooMany.replace('{0}', found).replace('{1}', this.length)
18457																		:
18458																		found?
18459																		text.foundNTimes.replace('{0}', found) 
18460																		: 
18461																		text.notFound
18462															}));
18463														
18464														renderOverlay(oly, this, ready);
18465													}
18466							}));
18467					},
18468				
18469				// Search date range
18470				
18471				searchDateRange = function(range) {
18472					
18473						if (range) {
18474							var options = getDateRangeOptions(range);
18475							
18476							getItemsByDate(options, function() {
18477									setHash({
18478											date:		range
18479										}, getDateRangeTitle(options));
18480									updateShares();
18481								});
18482						}
18483						
18484						return false;
18485					},
18486				
18487				// Search new images
18488				
18489				searchNew = function(days) {
18490						
18491						if (days) {
18492							
18493							getItemsByDate($.extend({}, settings.searchNew, { 
18494									start: 		nows / ONEDAY_S - days 
18495								}), function() {
18496									setHash({
18497											newimages:		days
18498										}, getDateReference(settings.searchNew['reference']));
18499									updateShares();
18500								});
18501						}
18502						
18503						return false;
18504					},
18505				
18506				// Prepare Search new functionality
18507				
18508				prepareSearchNew = function(f) {
18509					
18510						if (!f.length || !album) {
18511							return;
18512						}
18513						
18514						// Setting up defaults if something is missing
18515						
18516						settings.searchNew = $.extend({
18517								days: 				'3,30,90',
18518								depth: 				'folder',
18519								reference: 			'dateTaken',
18520								sinceLastVisit:		true
18521							}, settings.searchNew);
18522						
18523						var	days = settings.searchNew.days.split(',');
18524						
18525						f.append($('<label>', {
18526								text: 	getDateReference(settings.searchNew.reference)
18527							}));
18528								
18529						f = $('<div>', {
18530								'class':	'buttons'
18531							}).appendTo(f);
18532						
18533						for (var i = 0; i < days.length; i++) {
18534							f.append($('<a>', {
18535									'class': 	'small button',
18536									text: 		getTimespan(days[i])
18537								})
18538								.data('days', parseInt(days[i], 10))
18539								.on('click', function() {
18540										searchNew($(this).data('days'));
18541									}));
18542						}
18543								
18544						if (settings.searchNew.sinceLastVisit) {
18545							
18546							var start = $.cookie('lastVisit'),
18547								now = Math.round(new Date() / 1000);
18548							
18549							if (start && start < (now - ONEDAY_S)) {
18550								f.append($('<a>', {
18551										'class': 	'small button',
18552										text: 		text.sinceMyLastVisit
18553									})
18554									.on('click', function() {
18555											searchNew((now - start) / ONEDAY_S);
18556										}));
18557							}
18558							
18559							// Storing last visit date for 10 years
18560							$.cookie('lastVisit', now, 315360000);
18561						}
18562					},
18563									
18564				/********************************************************
18565				 *
18566				 *					Filtering & Sort
18567				 *
18568				 ********************************************************/
18569				
18570				// Dates are objects
18571				
18572				getDate = function(item, name) {
18573						if (item.hasOwnProperty(J.DATES)) {
18574							if (name === 'addedDate') {
18575								return item[J.DATES]['added'];
18576							} else if (name === 'originalDate') {
18577								return item[J.DATES]['dateTaken'];
18578							} else {
18579								return item[J.DATES]['fileModified'];
18580							}
18581						}
18582						
18583						return null;
18584					},
18585					
18586				// Some variables get remapped by the skin
18587				
18588				getMappedVal = function(item, name) {
18589					
18590						if (item.hasOwnProperty(name)) {
18591							// Generic case
18592							return item[name];
18593						}
18594						
18595						return null;
18596					},
18597					
18598				// Do filtering on current folder
18599				
18600				doFilter = function(e, ready) {
18601						var form = $(e.target).closest('form'),
18602							items = getAllItems(),
18603							cnt = items.length,
18604							sbd = {},
18605							tx = [];
18606						
18607						form.find('select:visible:not(:disabled),input[type=range],input[type=date]').each(function() {
18608								var self = $(this),
18609									name = this.name,
18610									label = form.find('label[for=' + name + ']').text(),
18611									type = self.data('type'),
18612									value = self.val(),
18613									display = function(val) {
18614											if (name ===  'rating') {
18615												return Math.round(val);
18616											} else if (name === 'fileSize') {
18617												return niceByte(val);
18618											} else if (type === 'date') {
18619												return (new Date(val * 1000)).toLocaleDateString(LOCALE);
18620											} else if (type === 'numeric') {
18621												if (self.data('unit') === 'seconds') {
18622													return niceTime(val);
18623												} else if (self.data('unit') === 'byte') {
18624													return niceByte(val);
18625												} else if (val >= 10) {
18626													return Math.round(val);
18627												} else if (val >= 1) {
18628													return val.toFixed(1);
18629												} else if (val >= 0.1) {
18630													return val.toFixed(2);
18631												}
18632												return val;
18633											}
18634											return toCurrency(val, currency);
18635										};
18636									
18637								if (type === 'numeric' || type === 'date' || type === 'financial') {
18638										
18639									var low = self.data('low'),
18640										high = self.data('high');
18641										
18642									if (low > this.min || high < this.max) {
18643										// Only if the range is narrower than the whole range
18644										
18645										if (name.endsWith('Date')) {
18646											// Date
18647											for (var i = items.length - 1, val; i >= 0; i--) {
18648												if ((val = getDate(items[i], name)) !== null && 
18649													(val >= low && val <= high)) {
18650													continue;
18651												}
18652												items.splice(i, 1);
18653											}
18654										} else if (JCAMERAFIELDS.indexOf(name) !== -1) {
18655											// Camera data fields
18656											if (name === 'exposureTime') {
18657												for (var i = items.length - 1, val; i >= 0; i--) {
18658													if (items[i].hasOwnProperty(J.CAMERA) &&
18659														items[i][J.CAMERA].hasOwnProperty(name) &&
18660														(val = getExposure(items[i][J.CAMERA][name])) &&
18661														(val >= low && val <= high)) {
18662														continue;
18663													}
18664													items.splice(i, 1);
18665												}
18666											} else if (name.startsWith('focalLength')) {
18667												for (var i = items.length - 1, val; i >= 0; i--) {
18668													if (items[i].hasOwnProperty(J.CAMERA) &&
18669														items[i][J.CAMERA].hasOwnProperty(name) &&
18670														!isNaN(val = parseFloat(items[i][J.CAMERA][name])) && 
18671														(val >= low && val <= high)) {
18672														continue;
18673													}
18674													items.splice(i, 1);
18675												}
18676											} else {
18677												for (var i = items.length - 1, val; i >= 0; i--) {
18678													if (items[i].hasOwnProperty(J.CAMERA) &&
18679														items[i][J.CAMERA].hasOwnProperty(name) &&
18680														!isNaN(val = parseFloat(items[i][J.CAMERA][name])) &&
18681														(val >= low && val <= high)) {
18682														continue;
18683													}
18684													items.splice(i, 1);
18685												}
18686											}
18687										} else if (name === 'rating') {
18688											// Rating
18689											for (var i = items.length - 1, val, r = settings.visitorRating? J.VISITORRATING : J.RATING; i >= 0; i--) {
18690												val = items[i][r] || 0;
18691												if (val >= low && val <= high) {
18692													continue;
18693												}
18694												items.splice(i, 1);
18695											}
18696										} else {
18697											// Generic
18698											for (var i = items.length - 1, val; i >= 0; i--) {
18699												if (!isNaN(val = parseFloat(items[i][name])) && 
18700													(val >= low && val <= high)) {
18701													continue;
18702												}
18703												items.splice(i, 1);
18704											}
18705										}
18706									
18707										tx.push(label + '=' + display(low) + '&ndash;' + display(high));
18708									}
18709									
18710								} else if (this.nodeName === 'SELECT' && value !== null && value.length) {
18711									
18712									// Only if at least one option is selected
18713																		
18714									if (!Array.isArray(value)) {
18715										value = [ value ];
18716									}
18717									
18718									if (JCAMERAFIELDS.indexOf(name) !== -1) {
18719										// Camera data :: no arrays expected
18720										for (var i = items.length - 1, val; i >= 0; i--) {
18721											if (items[i].hasOwnProperty(J.CAMERA) &&
18722												items[i][J.CAMERA].hasOwnProperty(name) &&
18723												(val = items[i][J.CAMERA][name]) !== null &&
18724												value.includes(val)) {
18725												continue;
18726											}
18727											items.splice(i, 1);
18728										}
18729										
18730									} else {
18731										// Generic case
18732										for (var i = items.length - 1, found, val; i >= 0; i--) {
18733											if (items[i].hasOwnProperty(name)) {
18734												val = items[i][name];
18735												if (Array.isArray(val)) {
18736													// Array e.g. keywords
18737													found = false;
18738													for (var j = 0; j < value.length; j++) {
18739														if (val.includes(value[j])) {
18740															//console.log(items[i]['name'] + ': ' + val);  
18741															found = true;
18742															break;
18743														}
18744													}
18745													if (found) {
18746														continue;
18747													}
18748												} else if (value.includes(val)) {
18749													continue;
18750												}
18751											}
18752											items.splice(i, 1);
18753										}
18754									}
18755									
18756									sbd[name] = value;
18757									
18758									// Header text addition
18759									if (this['multiple']) {
18760										tx.push(name + '=[' + value.join(',') + ']');  
18761									} else {
18762										tx.push(label + '=' + value);  
18763									}
18764								}
18765							});
18766						
18767						if (items.length < cnt) {
18768							
18769							// Only show overlay if at least one thumb was filtered out
18770							
18771							var oly = prepareOverlay({
18772										className:	'filtered-items',
18773										icon:		'icon-filter',
18774										title:		text.results
18775									});
18776								
18777							if (!isEmpty(sbd)) {
18778								oly.data('sbd', sbd);
18779							}
18780							
18781							setTimeout(function() {
18782									
18783									appendToOverlayTitle($('<small>', {
18784											html: 		tx.join(', ') + ': ' + text.foundNTimes.replace('{0}', items.length)
18785										}));
18786									
18787									oly.on('overlayRemoved', resetBaseFilters);
18788									
18789									renderOverlay(oly, items, ready);
18790									
18791								}, 50);
18792						}
18793						
18794						return false;
18795					},
18796				
18797				// Do sorting on current set of thumbs
18798				
18799				doSort = function(e, ready) {
18800						var el = this.selectedOptions[0],
18801							name = this.value;
18802						
18803						if (!el || !name) {
18804							return true;
18805						}
18806						
18807						var items = getAllItems(),
18808							data = $(el).data('sort'),
18809							type = data['type'],
18810							asc = data['ascending'],
18811							v1,
18812							v2;
18813							
18814						if (type === 'numeric' || type === 'financial' || type === 'date') {
18815							// Number
18816							
18817							if (name.endsWith('Date')) {
18818								// Date
18819								items = items.sort(function(i1, i2) {
18820										v1 = getDate(i1, name);
18821										v2 = getDate(i2, name);
18822										return asc? (v1 || 0) - (v2 || 0)
18823													:
18824													(v2 || 0) - (v1 || 0);
18825									});
18826								
18827							} else if (JCAMERAFIELDS.indexOf(name) !== -1) {
18828								// Camera data
18829								if (name === 'exposureTime') {
18830									items = items.sort(function(i1, i2) {
18831											v1 = (i1.hasOwnProperty(J.CAMERA) && i1[J.CAMERA].hasOwnProperty(name))? 
18832													i1[J.CAMERA][name] : 0;
18833											v2 = (i2.hasOwnProperty(J.CAMERA) && i2[J.CAMERA].hasOwnProperty(name))? 
18834													i2[J.CAMERA][name] : 0;
18835											return asc? (getExposure(v1 || 0) - getExposure(v2 || 0)) 
18836														: 
18837														(getExposure(v2 || 0) - getExposure(v1 || 0));
18838										});
18839								} else {
18840									items = items.sort(function(i1, i2) {
18841											v1 = (i1.hasOwnProperty(J.CAMERA) && i1[J.CAMERA].hasOwnProperty(name))? 
18842													parseFloat(i1[J.CAMERA][name]) : 0;
18843											v2 = (i2.hasOwnProperty(J.CAMERA) && i2[J.CAMERA].hasOwnProperty(name))? 
18844													parseFloat(i2[J.CAMERA][name]) : 0;
18845											return asc? ((v1 || 0) - (v2 || 0)) 
18846														: 
18847														((v2 || 0) - (v1 || 0));
18848										});
18849								}
18850								
18851							} else if (name === 'rating') {
18852								// Rating
18853								var r = settings.visitorRating? J.VISITORRATING : J.RATING;
18854								items = items.sort(function(i1, i2) {
18855										return asc? (i1[r] || 0) - (i2[r] || 0)
18856													:
18857													(i2[r] || 0) - (i1[r] || 0);
18858									});
18859								
18860							} else {
18861								// Every other numeric
18862								items = items.sort(function(i1, i2) {
18863										if (i1.hasOwnProperty(name) && i2.hasOwnProperty(name)) {
18864											return asc?	parseFloat(i1[name]) - parseFloat(i2[name])
18865														:
18866														parseFloat(i2[name]) - parseFloat(i1[name]);
18867										}
18868										return asc? (i1.hasOwnProperty(name)? 1 : 0)
18869													:
18870													(i2.hasOwnProperty(name)? -1 : 0);
18871									});
18872							}
18873							
18874						} else {
18875							// String
18876							
18877							items = items.sort(function(i1, i2) {
18878								if (i1.hasOwnProperty(name) && i2.hasOwnProperty(name)) {
18879									return asc?	(i1[name] + '').localeCompare(i2[name] + '')
18880												:
18881												(i2[name] + '').localeCompare(i1[name] + '');
18882								}
18883								return asc? (i1.hasOwnProperty(name)? 1 : 0)
18884											:
18885											(i2.hasOwnProperty(name)? -1 : 0);
18886							});
18887							
18888						}
18889						
18890						var oly = prepareOverlay({
18891									className:	'sorted-items',
18892									icon:		'icon-ordering',
18893									title:		text.sortedBy + '<small>' + data.label + ' (' + (data.ascending? text.ascending : text.descending) + ')</small>' 
18894								});
18895							
18896						setTimeout(function() {
18897								
18898								oly.on('overlayRemoved', resetBaseSort);
18899								
18900								renderOverlay(oly, items, ready);
18901							
18902							}, 50);
18903						
18904						return false;
18905					},
18906					
18907				// Read filter ranges from current overlay
18908				
18909				getFilterRanges = function(items, filter) {
18910						
18911						if (!filter || !items.length) {
18912							return;
18913						}
18914						
18915						var setMinMax = function(f, val) {
18916									if (f.hasOwnProperty('min') && f.hasOwnProperty('max')) {
18917										if (f.min > val) {
18918											f.min = val;
18919										} else if (f.max < val) {
18920											f.max = val;
18921										}
18922									} else {
18923										f.min = f.max = val;
18924									}
18925								};
18926								
18927						for (var name in filter) {
18928							
18929							var f = filter[name], 
18930								type = f['type'];
18931							
18932							if (type === 'numeric' || type === 'date' || type === 'financial') {
18933								
18934								if (name.endsWith('Date')) {
18935									// Date
18936									for (var i = items.length - 1, val; i >= 0; i--) {
18937										if ((val = getDate(items[i], name)) !== null) {
18938											setMinMax(f, val);
18939										}
18940									}
18941								} else if (JCAMERAFIELDS.indexOf(name) !== -1) {
18942									// Camera data fields
18943									if (name === 'exposureTime') {
18944										for (var i = items.length - 1, val; i >= 0; i--) {
18945											if (items[i].hasOwnProperty(J.CAMERA) &&
18946												items[i][J.CAMERA].hasOwnProperty(name) &&
18947												(val = getExposure(items[i][J.CAMERA][name]))) {
18948												setMinMax(f, val);
18949											}
18950										}
18951									} else if (name.startsWith('focalLength')) {
18952										for (var i = items.length - 1, val; i >= 0; i--) {
18953											if (items[i].hasOwnProperty(J.CAMERA) &&
18954												items[i][J.CAMERA].hasOwnProperty(name) &&
18955												!isNaN(val = parseFloat(items[i][J.CAMERA][name]))) {
18956												setMinMax(f, val);
18957											}
18958										}
18959									} else {
18960										for (var i = items.length - 1, val; i >= 0; i--) {
18961											if (items[i].hasOwnProperty(J.CAMERA) &&
18962												items[i][J.CAMERA].hasOwnProperty(name) &&
18963												!isNaN(val = parseFloat(items[i][J.CAMERA][name]))) {
18964												setMinMax(f, val);
18965											}
18966										}
18967									}
18968								} else if (name === 'rating') {
18969									// Rating
18970									for (var i = items.length - 1, val, r = settings.visitorRating? J.VISITORRATING : J.RATING; i >= 0; i--) {
18971										val = items[i][r] || 0;
18972										setMinMax(f, val);
18973									}
18974								} else {
18975									// Generic
18976									for (var i = items.length - 1, val; i >= 0; i--) {
18977										if (!isNaN(val = parseFloat(items[i][name]))) {
18978											setMinMax(f, val);
18979										}
18980									}
18981								}
18982								
18983							} else {
18984								// String
18985								if (!f.hasOwnProperty('values')) {
18986									f.values = [];
18987								}
18988								
18989								if (JCAMERAFIELDS.indexOf(name) !== -1) {
18990									// Camera data :: no arrays expected
18991									for (var i = 0, val; i < items.length; i++) {
18992										if (items[i].hasOwnProperty(J.CAMERA) &&
18993											items[i][J.CAMERA].hasOwnProperty(name)) {
18994											pushNew(f.values, (items[i][J.CAMERA][name] + '').trim());
18995										}
18996									}
18997								} else {
18998									for (var i = 0, val; i < items.length; i++) {
18999										if (items[i].hasOwnProperty(name)) {
19000											pushNew(f.values, items[i][name]);
19001										}
19002									}
19003									if (f.values.length > 1) {
19004										f.values = f.values.sort(function(a, b) { return a.localeCompare(b); });
19005									}
19006								}
19007							}
19008						}
19009						
19010						return filter;
19011					},
19012					
19013				// Enabling / disabling the reset button
19014				
19015				enableResetBtn = function(e) {
19016						var form = (typeof e === UNDEF || e.hasOwnProperty('target'))? $(this).closest('form') : e;
19017						form.find('button[type=reset]').removeClass('disabled');
19018					},
19019					
19020				disableResetBtn = function(e) {
19021						var form = (typeof e === UNDEF || e.hasOwnProperty('target'))? $(this).closest('form') : e;
19022						form.find('button[type=reset]').addClass('disabled');
19023					},
19024					
19025				// Reseting filters to defaults
19026				
19027				_resetFilters = function(form) {
19028					
19029						form.find('select').each(function() {
19030								$(this).prop('selectedIndex', this['multiple']? -1 : 0);
19031							});
19032						
19033						form.find('input[type=range]').each(function() {
19034								$(this).trigger('resetRange');
19035							});
19036						
19037						disableResetBtn(form);
19038						
19039					},
19040					
19041				// Reset filters after a form elemnt changed
19042				
19043				resetFilters = function(e) {
19044						_resetFilters($(e.target).closest('form'));
19045						return false;
19046					},
19047					
19048				// Reseting base filters to defaults
19049				
19050				resetBaseFilters = function() {
19051						resetBaseSort();
19052						_resetFilters($('#filters form.filter'));
19053					},
19054					
19055				// Reseting base filters to defaults
19056				
19057				resetBaseSort = function() {
19058						$('#filters form.sort select').prop('selectedIndex', 0);
19059					},
19060					
19061				// Preparing filters / sort
19062				
19063				prepareFilterBox = function(baseOly) {
19064					
19065						var items = getAllItems(),
19066							sbd = getOverlay().data('sbd'),
19067							filters = album.getPropertyObject(album.getCurrentFolder(), J.FILTERS, true),
19068							sort = album.getPropertyObject(album.getCurrentFolder(), J.SORT, true),
19069							filterSection = $('#filters');
19070							
19071						if ((!filters && !sort) || !items.length) {
19072							if (baseOly) {
19073								filterSection.hide();
19074							}
19075							
19076							return;
19077						}
19078						
19079						var	box = filterSection.find('.' + settings.boxClass),
19080							filterForm = box.find('form.filter'),
19081							sortForm = box.find('form.sort'),
19082							group,
19083							inp,
19084							lbl,
19085							type,
19086							buttons,
19087							btn;
19088							
19089						if (!baseOly) {
19090							// Preparing filtering on overlay
19091							
19092							box = $('<div>', {
19093									'class':		settings.contClass + ' ' + settings.boxClass + ' small-column',
19094									id:				'filters_' + getOverlayNS()
19095								}).appendTo(filterSection);
19096								
19097							box.append($('<h4>', {
19098									'class':		'icon-' + (filters? 'filter' : 'ordering'),
19099									text:			filterSection.find('h4').text()
19100								}));	
19101							
19102							if (filters) {
19103								filterForm = $('<form>', {
19104										'class':		'filter'
19105									}).appendTo(box);
19106							}
19107							
19108							if (sort) {
19109								sortForm = $('<form>', {
19110										'class':		'sort'
19111									}).appendTo(box);
19112							}
19113							
19114							// Attaching box to current overlay
19115							attachToOverlay(box);
19116							
19117						} else {
19118							
19119							box.addClass(settings.hideOnOverlayClass);
19120						}	
19121						
19122						buttons = $('<div>', {
19123								'class':	'buttons'
19124							}).appendTo(filterForm);
19125						
19126						if (filters) {
19127							
19128							// Getting filter range
19129							filters = getFilterRanges(items, filters);
19130							
19131							var m;
19132							
19133							for (var f in filters) {
19134								
19135								type = filters[f]['type'];
19136								
19137								group = $('<div>', {
19138										'class':		'group ' + ((type === 'multiple_string' || type === 'single_string')? 'select' : 'range') 
19139									}).insertBefore(buttons);
19140								
19141								if (filters[f]['label']) {
19142									lbl = $('<label>', {
19143											'for':			f,
19144											text:			filters[f]['label']
19145										}).appendTo(group);
19146								}
19147									
19148								if (type === 'multiple_string' || type === 'single_string') {
19149									
19150									if (f === 'rating') {
19151										filters[f].values = [ text.noRating, 1, 2, 3, 4, 5 ];
19152									}
19153									
19154									inp = $('<select>', {
19155											id:				f,
19156											name:			f
19157										}).appendTo(group);
19158									
19159									if (type === 'multiple_string') {
19160										inp.attr('multiple', true);
19161										inp.addTooltip(text.multipleSelectHint, { delay: 600, stay: 3000 });
19162									}
19163									
19164									if (!filters[f].hasOwnProperty('values') || !filters[f].values.length) { // || filters[f].values.length < 2) {
19165										
19166										group.remove();
19167										
19168									} else {
19169									
19170										// Select hint if more than one option
19171										if (baseOly || type === 'single_string' && filters[f].values.length > 1) {
19172											inp.prepend($('<option>', {
19173													text:			text.select,		// + ' ' + filters[f]['label'],
19174													disabled:		'',
19175													selected:		''
19176												}));
19177										}
19178										
19179										// Adding options
19180										if (filters[f].values) {
19181											for (var i = 0; i < filters[f].values.length; i++) {
19182												inp.append($('<option>', {
19183														text:			filters[f].values[i]
19184													}));
19185											}
19186										}
19187										
19188										if (!baseOly) {
19189											
19190											if (filters[f].values.length === 1) {
19191												// Disabling box if there's only one option
19192												inp.prop('disabled', true);
19193												inp.children().first().prop('selected', true);
19194											} else if (sbd && sbd.hasOwnProperty(f)) {
19195												// Preselect the previous filtering value(s)
19196												inp.children('option:enabled').each(function(i) {
19197														if (sbd[f].includes(filters[f].values[i])) {
19198															$(this).prop('selected', true);
19199														}
19200													});
19201											}
19202										}
19203										
19204										inp.on('change', enableResetBtn);
19205									}
19206									
19207								} else {
19208									
19209									inp = $('<input>', {
19210											id:			f,
19211											name:		f,
19212											type:		'range'
19213										})
19214										.data('type', type)
19215										.prependTo(group);
19216									
19217									if (f === 'rating') {
19218										filters[f]['step'] = 1;
19219										filters[f]['min'] = 0;
19220										filters[f]['max'] = 5;
19221									}
19222									
19223									if (!filters[f].hasOwnProperty('min') || !filters[f].hasOwnProperty('max') || filters[f]['min'] === filters[f]['max']) {
19224										
19225										group.remove();
19226										
19227									} else {
19228										
19229										if (type === 'financial' && (m = filters[f]['label'].match(/\S+\s+[\(\[](\S+)[\)\]]/))) {
19230										
19231											filters[f]['currency'] = m[1];
19232										
19233										} else if (type === 'numeric') {
19234											
19235											if (f === 'exposureTime' || f === 'shutterSpeed') {
19236												filters[f]['unit'] = 'seconds';
19237											} else if (f === 'fileSize') {
19238												filters[f]['unit'] = 'byte';
19239												filters[f]['step'] = 1;
19240											}
19241										}
19242										filters[f].onChanged = enableResetBtn;
19243										filters[f].onReseted = disableResetBtn;
19244										
19245										inp.rangeSlider(filters[f]);
19246									}
19247								}
19248									
19249								inp.attr('data-type', type || 'single_string');
19250							}
19251							
19252							// Adding buttons
19253							
19254							if ((inp = filterForm.find('input,select')).length) {
19255								
19256								filterForm.data('single', inp.length === 1 && inp[0].nodeName === 'SELECT' && !inp.attr('multiple'));
19257								
19258								if (!(baseOly && filterForm.data('single'))) {
19259									// Reset button
19260									btn = $('<button>', {
19261											type:			'reset',
19262											'class':		(baseOly? 'disabled secondary' : 'alert') + ' button',
19263											text:			' ' + text.reset
19264										}).appendTo(buttons);
19265									
19266										if (baseOly) {
19267											btn.on('click.' + ns, resetFilters);
19268										} else {
19269											btn.on('click.' + ns, function() {
19270													resetBaseFilters();
19271													removeOverlay();
19272												});
19273										}
19274								}
19275									
19276								if (filterForm.data('single')) {
19277									// Single combo box -> Update instantly on change
19278									inp.on('change.' + ns, doFilter);
19279									
19280								} else {
19281									// Multiple inputs or not single combo box -> Use 'Search' button
19282									btn = filterForm.find('button[type=submit]');
19283									
19284									if (!btn.length) {
19285										btn = $('<button>', {
19286												type:			'submit',
19287												'class':		'button',
19288												text:			' ' + text.search
19289											}).appendTo(buttons);
19290									}
19291									
19292									btn.on('click.' + ns, doFilter);
19293								}
19294								
19295								filterForm.on('submit.' + ns, doFilter);
19296							}
19297						}
19298						
19299						if (buttons.is(':empty')) {
19300							buttons.remove();
19301						}
19302						
19303						if (sort) {
19304														
19305							lbl = $('<label>', {
19306									'class':		'icon-ordering',
19307									'for':			'sortby',
19308									text:			' '
19309								}).appendTo(sortForm);
19310								
19311							inp = $('<select>', {
19312									'class':		'small',
19313									id:				'sortby',
19314									name:			'sortby'
19315								}).appendTo(sortForm);
19316							
19317							inp.prepend($('<option>', {
19318									text:			text.sortBy,
19319									disabled:		'',
19320									selected:		''
19321								}));
19322							
19323							for (var s in sort) {
19324								
19325								inp.append($('<option>', {
19326										html:			'&#8613; ' + sort[s].label,
19327										value:			s
19328									}).data('sort', {
19329										type:			sort[s].type,
19330										label:			sort[s].label,
19331										ascending:		true
19332									}));
19333								
19334								inp.append($('<option>', {
19335										html:			'&#8615; ' + sort[s].label,
19336										value:			s
19337									}).data('sort', {
19338										type:			sort[s].type,
19339										label:			sort[s].label,
19340										ascending:		false
19341									}));
19342							}
19343						
19344							inp.on('change.' + ns, doSort);
19345						}
19346						
19347						if ((!sortForm || !sortForm.length || sortForm.is(':empty')) && 
19348							(!filterForm || !filterForm.length || filterForm.is(':empty'))) {
19349							filterSection.hide();
19350						}
19351					},
19352				
19353				/********************************************************
19354				 *
19355				 *						Controls
19356				 *
19357				 ********************************************************/
19358				
19359				// Initializing ZIP download button
19360				
19361				initZip = function() {
19362				
19363						panels.find('.zip-cont a.button').each(function() {
19364								var btn = $(this);
19365								
19366								getFileSize(btn.attr('href'), function(size) {
19367										if (size) {
19368											btn.append('<span class="badge">' + niceByte(size) + '</span>');
19369										}
19370									});
19371							});
19372					},
19373
19374				/********************************************************
19375				 *
19376				 *			Initialization of misc functions
19377				 *
19378				 ********************************************************/
19379										
19380				// Right click blocking
19381				
19382				avoidRightClick = function() {
19383						getImages().add(getFolders()).on('contextmenu', 'img', function(e) {
19384								e.preventDefault();
19385								return false;
19386							});
19387					},
19388					
19389				// Initializing background music
19390				
19391				prepareBackgroundMusic = function(player) {
19392					
19393						if (player.length) {
19394							player.audioPlayer({
19395									rootPath: 	settings.rootPath
19396								});
19397						}
19398					},
19399					
19400				// Updating the share buttons after a state change
19401				
19402				updateShares = function() {
19403					
19404						if (settings.hasOwnProperty('share')) {
19405							
19406							if (lightboxOn()) {
19407								// Lightbox: sharing actual image
19408								var item = getCurrentItem();
19409								
19410								if (item) {
19411									$(settings.share.hook).trigger('updateLinks', {
19412											sites: 			settings.share,
19413											title: 			item[J.TITLE] || item[J.NAME].stripExt().replace(/[-_]/g, ' ').capitalize(),
19414											description: 	item[J.COMMENT] || '',
19415											image: 			album.getOptimalImagePath(item, [ 1200, 800 ]),
19416											href: 			album.getAbsolutePath(item)
19417										});
19418								}
19419							} else {
19420								// Index page: reset to page sharing
19421								$(settings.share.hook).trigger('updateLinks', { href: window.location.href });
19422							}
19423						}	
19424					},
19425				
19426				// Initializing Social sharing
19427				
19428				prepareShares = function(opt) {
19429						
19430						if (typeof opt === UNDEF) return;
19431
19432						$.fn.renderShares.defaults.buttonTheme = opt['buttonTheme'] || 'dark';
19433						$.fn.renderShares.defaults.indexName = settings.indexName;
19434						$.fn.renderShares.defaults.facebookAppId = opt['facebookAppId'] || '';
19435						$.fn.renderShares.defaults.albumTitle = album.getAlbumTitle(); 
19436						
19437						if (opt.hasOwnProperty('emailSubject')) {
19438							$.fn.renderShares.defaults.emailSubject = opt.emailSubject;
19439						}
19440						
19441						if (opt.hasOwnProperty('emailBody')) {
19442							$.fn.renderShares.defaults.emailBody = opt.emailBody;
19443						}
19444						
19445						$(opt['hook']).renderShares(opt);
19446						
19447						if (opt.hasOwnProperty('sites')) {
19448							$.fn.lightbox.defaults.share = opt.sites;
19449							$.fn.lightbox.defaults.shareHook = opt['hook'];
19450						}
19451					},
19452				
19453				// Reading out map locations
19454				
19455				getMarkers = function(images) {
19456						// Reading out map locations
19457						var markers = [];
19458						
19459						for (var i = 0, l; i < images.length; i++) {
19460							if (images[i] && (l = album.getLocation(images[i]))) {
19461								markers.push({
19462									title:		[((i + 1) + '.'), images[i]['title'], images[i]['comment']].filter(Boolean).join(' '),
19463									pos: 		l,
19464									link:		images[i].path
19465								});
19466							}
19467						}
19468						
19469						return markers;
19470					},
19471						
19472				// Preparing map
19473				
19474				prepareMap = function(images, opt) {
19475					
19476						if (images && images.length) {
19477							opt.markers = getMarkers(images);
19478						}
19479							
19480						if (opt.hasOwnProperty('markers') && opt.markers.length || opt.hasOwnProperty('overlay')) {
19481							var	mapRoot = getMapRoot(true);
19482						
19483							if (mapRoot.length) {
19484								if (!mapRoot.data('inited')) {
19485									// Add map only once
19486									opt.resPath = settings.resPath;
19487									
19488									opt.onMarkerClick = function() {
19489											if (this.hasOwnProperty('link')) {
19490												var el = getCardByName(this.link);
19491													
19492												if (el.length) {
19493													if (isFullscreen()) {
19494														exitFullscreen();
19495													}
19496													getOverlay().trigger('setactivecard', el);
19497													lightboxLoad(el);
19498												}
19499											}
19500										};
19501										
19502									if (opt.hasOwnProperty('apiKey')) {
19503										// Google Maps
19504										opt.streetViewControl = opt.typeControl = (window.innerWidth >= 640) && (window.innerHeight >= 640);
19505									}
19506										
19507									// Unhide Map section
19508									mapRoot.closest('section').removeClass('hidden');
19509									// Calling Map library
19510									mapRoot.addMap(opt);
19511									// Set inited flag
19512									mapRoot.data('inited', true);
19513								}
19514							
19515								main.removeClass(settings.noMapClass).addClass(settings.hasMapClass);
19516								return;
19517							}
19518						} 
19519						
19520						main.removeClass(settings.hasMapClass).addClass(settings.noMapClass);
19521					},
19522				
19523				// Preparing shopping cart
19524				
19525				prepareShoppingCart = function() {
19526					
19527						// Initializing the cart
19528						if (shopRoot.length) {
19529							// Using the paypal cart's currency as global currency
19530							currency = album.getCurrency();
19531							
19532							// Set rangeslider's default currency
19533							if ($.fn.hasOwnProperty('rangeSlider') && currency) {
19534								$.fn.rangeSlider.defaults.currency = currency;
19535							}
19536						
19537							// Calling Paypal module
19538							shopRoot.paypal(album, {
19539									resPath:				settings.resPath,
19540									relPath:				settings.relPath,
19541									rootPath:				settings.rootPath,
19542									currency:				currency,
19543									getSelected: 			getSelectedItems,
19544									selectNone:				function() {
19545																getBaseOverlay().trigger('selectNone');
19546															},
19547									getCurrent:				getCurrentItem,
19548									useFloatButton:			!(shopRoot.parents('.shop-cont[data-sticky]').length || document.body.clientWidth >= 640)
19549								});
19550							
19551							// "Add selected" button
19552							shopRoot.find('.' + settings.addSelectedClass).on('click', function() {
19553									shopRoot.trigger('addItems', getSelectedItems());
19554									getBaseOverlay().trigger('selectNone');
19555									return false;
19556								});
19557							
19558							// Ensuring lightbox has 'shop' property
19559							if (!$.fn.lightbox.defaults.hasOwnProperty('shop')) {
19560								$.fn.lightbox.defaults.shop = {};
19561							}
19562							
19563							// Setting lightbox's shop root variable
19564							$.fn.lightbox.defaults.shop.root = shopRoot;
19565						}
19566					
19567					},
19568					
19569				// Preparing feedback
19570				
19571				prepareFeedback = function() {
19572					
19573						// Initializing the feedback cart
19574						if (feedbackRoot.length) {
19575								
19576							// Calling Feedback module
19577							feedbackRoot.feedback(album, $.extend({}, settings.feedback, {
19578									resPath:				settings.resPath,
19579									relPath:				settings.relPath,
19580									rootPath:				settings.rootPath,
19581									getSelected:			getSelectedItems,
19582									selectNone:				function() {
19583																getBaseOverlay().trigger('selectNone');
19584															},
19585									getCurrent:				getCurrentItem,
19586									useFloatButton:			!(feedbackRoot.parents('.feedback-cont[data-sticky]').length || document.body.clientWidth >= 640)			
19587								}));
19588							
19589							// "Add selected" button
19590							feedbackRoot.find('.' + settings.addSelectedClass).on('click', function() {
19591									feedbackRoot.trigger('addItems', getSelectedItems());
19592									getBaseOverlay().trigger('selectNone');
19593									return false;
19594								});
19595							
19596							// Ensuring lightbox has 'feedback' property
19597							if (!$.fn.lightbox.defaults.hasOwnProperty('feedback')) {
19598								$.fn.lightbox.defaults.feedback = {};
19599							}
19600							// Setting lightbox's feedback root variable
19601							$.fn.lightbox.defaults.feedback.root = feedbackRoot;
19602							$.fn.lightbox.defaults.feedbackBtnLabel = settings.feedback['viewBtnLabel'];
19603							$.fn.lightbox.defaults.feedbackAddBtnLabel = settings.feedback['addBtnLabel'];
19604							$.fn.lightbox.defaults.feedbackBtnTooltip = settings.feedback['addBtnTooltip'];
19605						}
19606					},
19607					
19608				/********************************************************
19609				 *
19610				 *					Index page initialization
19611				 *
19612				 ********************************************************/
19613				
19614				prepareIndex = function() {
19615					
19616						if (!album) {
19617							return;
19618						}
19619						
19620						var images = album.getImages(),
19621							folders = getFolders(),
19622							hasImages = images.length > 0,
19623							hasFolders = folders.length > 0,
19624							hasShop = $.fn.hasOwnProperty('paypal') && album.hasShop(),
19625							hasFeedback = settings.hasOwnProperty('feedback');
19626						
19627						// Setting has-images class and data flag
19628						main.data('baseHasImages', hasImages);
19629						toggleHasImages(hasImages);
19630						
19631						if (hasImages) {
19632							// Removing thumbnail links right away
19633							getBaseImages().each(function() {
19634									this.dataset.href = this.href;
19635									this.href = '';
19636								});
19637						}
19638			
19639						// Shopping cart initialization
19640						if (hasShop) {
19641							shopRoot = $('#shop-root');
19642							shopBox = $('#shop-cont,#shop-box').eq(0);
19643							prepareShoppingCart();				
19644						} else {
19645							shopRoot = $();
19646							shopBox = $();
19647						}
19648						
19649						// Feedback initialization
19650						if (hasFeedback) {
19651							feedbackRoot = $('#feedback-root');
19652							feedbackBox = $('#feedback');
19653							prepareFeedback();
19654						} else {
19655							feedbackRoot = $();
19656							feedbackBox = $();
19657						}
19658						
19659						// Selection box initialization
19660						if (hasShop || hasFeedback) {
19661							// "Select all" button
19662							shopBox.add(feedbackBox).find('.' + settings.selectAllClass).on('click', function() {
19663									getOverlay().trigger('selectAll');
19664									return false;
19665								});
19666							
19667							// "Select none" button
19668							shopBox.add(feedbackBox).find('.' + settings.selectNoneClass).on('click', function() {
19669									getOverlay().trigger('selectNone');
19670									return false;
19671								});
19672						}
19673						
19674						// Initializing folders
19675						if (hasFolders) {
19676							prepareFolders(folders, lazyloadFolders);
19677						}
19678						
19679						// Rendering thumbnails
19680						if (hasImages) {
19681							// Preparing images on the base overlay
19682							renderImages(getBaseOverlay(), images, prepareImages);
19683							// Start slideshow wiring
19684							$(settings.startSlideshowHook).on('click', startSlideshow);
19685							                                           
19686						} else {
19687							// no images, check if inited by hash
19688							if (window.location.hash) {
19689								// we should check the search / tags / newimages filter
19690								stateChange();
19691							}
19692							// Hide Start sliodeshow button
19693							$(settings.startSlideshowHook).hide();
19694						}
19695												
19696						// Collecting tags if gathered only from this folder
19697						if (settings.hasOwnProperty('tagCloud') && settings.tagCloud['depth'] === 'current') {
19698							album.collectTags($.extend(settings.tagCloud, {
19699									ready:		function() {
19700														prepareTagCloud(this);
19701													}
19702								}));
19703						}
19704						
19705						// Search 
19706						if (settings.hasOwnProperty('search')) {
19707							prepareSearch($(settings.search.hook));
19708						}
19709						
19710						// Search new
19711						if (settings.hasOwnProperty('searchNew')) {
19712							prepareSearchNew($(settings.searchNew.hook));
19713						}
19714						
19715						// Preparing share buttons
19716						if (settings.hasOwnProperty('share')) {
19717							prepareShares(settings.share);
19718						}
19719						
19720						var slider = $('[data-slider]');
19721						
19722						if (slider.length) {
19723							var sls = slider.data('slider') || {},
19724								dim = [ slider.innerWidth(), slider.innerHeight() ];
19725							
19726							if (sls.hasOwnProperty('names')) {
19727								
19728								for (var i = 0; i < sls.names.length; i++) {
19729									sls.names[i] = [ settings.relPath, sls.names[i] ].filter(Boolean).join('/');
19730								}
19731								
19732								// Selected images
19733								album.collectByPath({
19734										folder:		settings.relPath,
19735										paths:		sls.names,
19736										levels:		0,
19737										ready:		function() {
19738															sls.src = [];
19739															for (var i = 0; i < this.length; i++) {
19740																sls.src.push(album.getOptimalImagePath(this[i], dim, true, true));
19741															}
19742															slider.easySlider(sls);
19743														}
19744									});
19745								
19746							} else {
19747								
19748								album.collectNItem({
19749										folder:		settings.relPath,
19750										max:		50,
19751										quick:		true,
19752										levels:		2,
19753										include:	'images',
19754										sortBy:		'random',
19755										arRange:	[ Math.min(1, 0.5 * dim[0] / dim[1]), Math.max(1, 2 * dim[0] / dim[1]) ],
19756										ready:		function() {
19757															sls.src = [];
19758															for (var i = 0; i < this.length; i++) {
19759																sls.src.push(album.getOptimalImagePath(this[i], dim, true, true));
19760															}
19761															slider.easySlider(sls);
19762														}
19763									});
19764							}
19765						}
19766						
19767						// Adding map on embedded custom pages
19768						var customMap = $('.pages-cont [data-map]');
19769						
19770						if (customMap.length && settings.hasOwnProperty('map')) {
19771							settings.map.resPath = settings.resPath;
19772							customMap.addMap($.extend({}, settings.map, customMap.data('map')));
19773						}
19774						
19775						// Custom query (eg. new images sample box)
19776						var queryTarget = $('[data-jalbum-query]');
19777						
19778						if (queryTarget.length) {
19779							// Reding settings from data attribute
19780							var qsettings = queryTarget.data('jalbum-query');
19781							
19782							if (qsettings.hasOwnProperty('fn')) {
19783								// Calling the jalbum.album function
19784								album[qsettings.fn]($.extend(qsettings, {
19785											ready:		function(opt) {
19786																if (this.length) {
19787												
19788																	for (var i = 0, a, item, mx = Math.min(this.length, 10); i < mx; i++) {
19789																		
19790																		item = this[i];
19791																		
19792																		a = $('<a>', {
19793																				href:	album.getLink(item)
19794																			}).appendTo(queryTarget);
19795																		
19796																		a.append($('<img>', {
19797																				src:		album.getThumbPath(item),/*
19798																				width:		item[J.THUMB][J.WIDTH],
19799																				height:		item[J.THUMB][J.HEIGHT],*/
19800																				alt:		item[J.TITLE] || item[J.NAME]
19801																			}));
19802																	}
19803																	
19804																	if (this.length > 10) {
19805																		queryTarget.append($('<a>', {
19806																				'class':	'more',
19807																				href:		qsettings.linkMore,
19808																				text:		'+' + (this.length - 10)
19809																			}));
19810																	}
19811																	
19812																	if (settings.rightClickProtect) {
19813																		queryTarget.on('contextmenu', 'img', function(e) {
19814																				e.preventDefault();
19815																				return false;
19816																			});
19817																	}
19818																} else {
19819																	queryTarget.closest('.newimages-page').hide();
19820																}
19821															}
19822								
19823									}));
19824							}
19825						}
19826					},
19827				
19828				/********************************************************
19829				 *
19830				 *					Custom page initialization
19831				 *
19832				 ********************************************************/
19833				
19834				preparePage = function() {
19835						// Page :: initializing functions
19836						// Search 
19837						if (settings.hasOwnProperty('search')) {
19838							prepareSearch($(settings.search.hook));
19839						}
19840						
19841						// Preparing share buttons
19842						if (settings.hasOwnProperty('share')) {
19843							prepareShares(settings.share);
19844						}
19845						
19846						settings.pageName = window.location.pathname.getFile();
19847						
19848						var paths = [];
19849						
19850						getCards().each(function() {
19851								var a = $(this).find('.' + settings.thumbClass);
19852								a.data('href', a.attr('href'));
19853								a.attr('href', '');
19854								// Collecting paths from thumbnails
19855								paths.push($(this).data(J.PATH));
19856							});
19857						
19858						if (paths.length) {
19859						
19860							setOverlayNS();
19861							
19862							album.collectByPath({
19863									paths:		paths,
19864									ready:		function() {
19865														// Rendering thumbnails
19866														if (this.length) {
19867															
19868															// Setting has-images class
19869															main.data('baseHasImages', true);
19870															toggleHasImages(true);
19871															
19872															// Preparing images on the base overlay
19873															renderImages(getBaseOverlay(), this, prepareImages);
19874															
19875														} else {
19876															// Setting has-images class
19877															toggleHasImages(false);
19878														}
19879													}
19880								});
19881						} else {
19882							var map = $('[data-map]');
19883							
19884							if (map.length) {
19885								prepareMap(null, $.extend({}, settings.map, map.data('map')));
19886							}
19887							// Setting has-images class
19888							main.data('baseHasImages', false);
19889							toggleHasImages(false);
19890						}
19891					},
19892				
19893				/********************************************************
19894				 *
19895				 *	 Preparing functions that need the full album tree
19896				 *
19897				 ********************************************************/
19898				
19899				prepareDeepFunctions = function() {
19900					
19901						// Collecting tags if gathered from deep tree
19902						if (settings.hasOwnProperty('tagCloud') && settings.tagCloud['depth'] !== 'current') {
19903							album.collectTags($.extend({}, settings.tagCloud, {
19904									ready:		function() {
19905														prepareTagCloud(this);
19906													}
19907								}));
19908						}
19909					},
19910					
19911				/********************************************************
19912				 *
19913				 *					Preparing folders
19914				 *
19915				 ********************************************************/
19916				
19917				// Marking new folders
19918				
19919				markFoldersNew = function() {
19920						
19921						if (!album) {
19922							return;
19923						}
19924						
19925						var folders = album.getFoldersAndWeblocs(),
19926							secs 	= settings.markNew['days'] * ONEDAY_S,
19927							s,
19928							d;
19929						
19930						getFolders().each(function(i) {
19931								
19932								if (i < folders.length) {
19933									d = folders[i][J.DATES];
19934									
19935									if (d) {
19936										d = d[settings.markNew['reference']] || (d[J.DATERANGE] && (d[J.DATERANGE][1] || d[J.DATERANGE][0]));
19937										
19938										if (d && (nows - d) < secs) {
19939											s = settings.markNew.text?
19940												$('<span>', {
19941														'class':	'new-image',
19942														text:		settings.markNew.text
19943													})
19944												:
19945												$('<span>', {
19946														'class':	'icon-new-fill new-image'
19947													});
19948											
19949											if (!TOUCHENABLED) {
19950												s.data('tooltip', (new Date(d * 1000)).toLocaleDateString(LOCALE)).addTooltip({ delay: 100 });
19951											}
19952											
19953											$(this).find('.' + settings.thumbClass).eq(0).append(s);
19954										}
19955									}
19956								}
19957							});
19958					},
19959				
19960				// Loading visible folder thumbs :: only on base overlay
19961				
19962				lazyloadFoldersTimeout = null,										// Lazyload tracking
19963				lazyloadFoldersInProgress = false,									// lock to avoid multiple calls
19964				
19965				lazyloadFolders = function() {
19966					
19967						if (lazyloadFoldersInProgress || getLightbox().is(':visible') || hasOverlay()) {
19968							clearTimeout(lazyloadFoldersTimeout);
19969							return;
19970						}
19971						
19972						if (lazyloadFoldersTimeout) {
19973							clearTimeout(lazyloadFoldersTimeout);
19974							lazyloadFoldersTimeout = null;
19975						}
19976						
19977						var st = $window.scrollTop();
19978						
19979						if (Math.abs(st - scrollTop) > 20) {
19980							// Scrolling in progress: wait until finishes
19981							scrollTop = st;
19982							lazyloadFoldersTimeout = setTimeout(lazyloadFolders, settings.scrollCheckInterval);
19983							return;
19984						}
19985						
19986						// Getting folders to load
19987						var folders = getFolders('.' + settings.lazyloadClass);
19988						
19989						if (folders.length) {
19990							var wh = $window.height();
19991						
19992							lazyloadFoldersInProgress = true;
19993							
19994							folders.each(function() {
19995									var card = $(this),
19996										tt = card.offset().top,
19997										th = card.height();
19998										
19999									if ((tt + th) > (scrollTop - wh)) {
20000										// Below 1 screens above scroll pos
20001										if (tt < (scrollTop + wh)) {
20002											loadFolderThumb(card);
20003										} else {
20004											// Far below: stop loading more
20005											return false;
20006										}
20007									}
20008								});
20009							
20010							// perpetrating lazyload
20011							clearTimeout(lazyloadFoldersTimeout);
20012							lazyloadFoldersTimeout = setTimeout(lazyloadFolders, settings.scrollCheckInterval);
20013							
20014						} else {
20015							
20016							$window.off('.lazy');
20017						}
20018						
20019						lazyloadFoldersInProgress = false;
20020						scrollTop = st;
20021					},
20022					
20023				// Reading counts from a <span class="image">N images</span><span class="audio">N audios</span><span class="video">N videos</span>
20024				
20025				readCounts = function(el) {
20026						var cnt = {
20027									images:		0,
20028									audios:		0,
20029									videos:		0,
20030									others:		0
20031								};
20032								
20033						el.children().each(function(i) {
20034								var c = this.innerText.split(/\W/),
20035									t = this.classList[0];
20036								
20037								if (!t && c.length > 1) {
20038									switch (c[1]) {
20039										case text.image:
20040										case text.images:
20041											t = 'images';
20042											break;
20043										case text.audio:
20044										case text.audios:
20045											t = 'audios';
20046											break;
20047										case text.video:
20048										case text.videos:
20049											t = 'videos';
20050											break;
20051										case text.other:
20052										case text.others:
20053											t = 'others';
20054											break;
20055										default:
20056											t = 'images';
20057									}
20058								}
20059										
20060								cnt[t] += parseInt(c[0]) || 0;
20061							});
20062						
20063						return cnt;
20064					},
20065					
20066				// Calculates the difference between two count objects
20067				
20068				getCountDiff = function(c1, c2) {
20069						var diff = {
20070									images:		0,
20071									audios:		0,
20072									videos:		0,
20073									others:		0
20074								};
20075								
20076						for (var c in c1) {
20077							if (c2.hasOwnProperty(c) && c1[c] !== c2[c]) {
20078								diff[c] = c1[c] - c2[c];
20079							}
20080						}
20081						
20082						return diff;
20083					},
20084					
20085				// CHeck if any difference was found
20086				
20087				allZero = function(diff) {
20088					
20089						for (var c in diff) {
20090							if (diff[c] !== 0) {
20091								return false;
20092							}
20093						}
20094						
20095						return true;
20096					},
20097					
20098				// Creates HTML formatted counts: <span class="image">N images</span><span class="audio">N audios</span><span class="video">N videos</span>
20099				
20100				getFormattedCounts = function(cnt) {
20101						var c = '';
20102						
20103						if (cnt['images']) {
20104							c += '<span class="image">' + cnt['images'] + '&nbsp;' + translate(cnt['images'] > 1? 'images':'image') + '</span>';
20105						}
20106						if (cnt['audios']) {
20107							c += '<span class="audio">' + cnt['audios'] + '&nbsp;' + translate(cnt['audios'] > 1? 'audios':'audio') + '</span>';
20108						}
20109						if (cnt['videos']) {
20110							c += '<span class="video">' + cnt['videos'] + '&nbsp;' + translate(cnt['videos'] > 1? 'videos':'video') + '</span>';
20111						}
20112						if (cnt['others']) {
20113							c += '<span class="other">' + cnt['others'] + '&nbsp;' + translate(cnt['others'] > 1? 'others':'other') + '</span>';
20114						}
20115						
20116						return c;
20117					},
20118					
20119				// Modifying the total 
20120					
20121				fixTotalImages = function(diff) {
20122						var el = $('.footer .counts'),
20123							cnt = readCounts(el);
20124							
20125						for (var c in cnt) {
20126							if (diff[c] !== 0) {
20127								cnt[c] += diff[c];
20128							}
20129						}
20130						
20131						//console.log('Diff: ' + JSON.stringify(diff) + ' cnt: ' + JSON.stringify(cnt));
20132						
20133						el.empty().append(getFormattedCounts(cnt));
20134					},
20135					
20136				// Preparing folders
20137			
20138				prepareFolders = function(folders, doneFn) { 
20139					
20140						if (typeof folders === UNDEF || !folders.length) {
20141							return;
20142						}
20143						
20144						if (settings.hasOwnProperty('markNew')) {
20145							// Marking new folders
20146							setTimeout(markFoldersNew, 300);
20147						}
20148						
20149						setTimeout(function() {
20150								// Start lazy load
20151								//lazyloadFolders();
20152								
20153								/*
20154								// Removing prelaod class on folders already loaded
20155								folders.filter('.' + settings.preloadClass + ':first-child img')
20156									.one('load', function() {
20157											$(this).parents('.' + settings.preloadClass).removeClass(settings.preloadClass);
20158										});
20159								*/
20160								if ((!LOCAL || DEBUG) && settings.hasOwnProperty('weblocationCounts') && settings.weblocationCounts) {
20161									// Adding counts to weblocations
20162									folders.filter('.webloc.has-count').each(function() {
20163											var caption = $(this).children('.caption'),					// Tha caption
20164												counts = readCounts(caption.find('.counts')),			// Saving original counts
20165												diff;
20166											
20167											loadExternal($(this).children('.thumb').attr('href'), function() {
20168													
20169													if (typeof this === OBJECT && this.hasOwnProperty(J.DEEPCOUNTERS)) {
20170														diff = getCountDiff(this[J.DEEPCOUNTERS], counts);
20171														
20172														if (!allZero(diff)) {
20173															
20174															caption.find('.counts')
20175																.empty()
20176																.append(getFormattedCounts(this[J.DEEPCOUNTERS]));
20177																	
20178															fixTotalImages(diff);
20179														}
20180													}
20181												});
20182										});
20183								}
20184								
20185								folders.find('.mosaic').each(function() {
20186										var th = $(this).siblings('.thumb');
20187											
20188										$(this).children().each(function(i) {
20189											var src = $(this).data('imgsrc') || $(this).children('img').eq(0).attr('src') || $(this).data('src');
20190												
20191											if (src) {
20192												var mi = $('<img>', {
20193														'class':	'mosimg hide-image',
20194														src:		src.replace('/thumbs/', '/slides/'),
20195														id:			'mi' + i
20196													}).appendTo(th);
20197												
20198												$(this).on('mouseenter', function(e) {
20199													setTimeout(function() {
20200															mi.removeClass('hide-image').addClass('show-image');
20201														}, 100);
20202												}).on('mouseleave', function(e) {
20203													setTimeout(function() {
20204															mi.addClass('hide-image').removeClass('show-image');
20205														}, 200);
20206												});
20207											}
20208										});
20209									});
20210								
20211							}, 500);
20212						
20213						if (typeof doneFn === FUNCTION) {
20214							doneFn.call(self[0]);
20215						}
20216					},
20217				
20218				/********************************************************
20219				 *
20220				 *					Initializing album
20221				 *
20222				 ********************************************************/
20223				
20224				initAlbum = function() {
20225					
20226						if (typeof Album === UNDEF) {
20227							console.log('Critical Error: Missing jalbum.album.js library!');
20228							return;
20229						}
20230						
20231						var needsDeep = settings.hasOwnProperty('tagCloud') && settings.tagCloud['depth'] !== 'current' || 
20232										settings.pageType === 'page' && settings.originalPage === 'newimages',
20233							params = {
20234									makeDate:			settings['makeDate'],
20235									rootPath:			settings['rootPath'],
20236									relPath:			settings['relPath'],
20237									hiDpiImages:		settings['hiDpiImages'] || false,
20238									hiDpiThumbs:		settings['hiDpiThumbs'] || false,
20239									folderImageFile:	settings.folderImageFileName,
20240									folderThumbFile:	settings.folderThumbFileName,
20241									thumbDims:			settings['thumbDims'] || [ 418, 209 ],
20242									thumbsFillFrame:	settings['thumbsFill'] || false,
20243									ready: 				(settings.pageType === 'index')? prepareIndex : preparePage,
20244									loadDeep:			needsDeep,
20245									fatalError:			function(txt, attr) {
20246																$body.modal((typeof attr === UNDEF)? translate(txt) : translate(txt).replace(/\{0\}/g, attr), {
20247																		title:		text.fatalError,
20248																		icon:		'error',
20249																		'class':	'small alert'
20250																	});
20251															},
20252									locale:				settings['locale'] || 'en-US'
20253								};
20254							
20255						if (settings.indexName !== $.fn.skin.defaults.indexName) {
20256							params.indexName = settings['indexName'];
20257						}
20258						
20259						if (settings.audioPoster !== $.fn.skin.defaults.audioPoster) {
20260							params.audioPoster = settings['audioPoster'];
20261						}
20262							
20263						if (settings.videoPoster !== $.fn.skin.defaults.videoPoster) {
20264							params.videoPoster = settings['videoPoster'];
20265						}
20266						
20267						if (settings.useOriginals) {
20268							params.useOriginals = true;
20269						}
20270						
20271						if (needsDeep) {
20272							params.deepReady = prepareDeepFunctions;
20273						}
20274						
20275						album = new Album($, params);
20276					},
20277					
20278				// Main initializing function
20279				
20280				init = function() {
20281						
20282						// Removing search string, e.g. FB data
20283						if (window.location.search) {
20284							removeSearch();
20285						}
20286						
20287						// Restoring scroll position, saving on unload
20288						restoreScrollPosition();
20289						$(window).on('beforeunload.' + ns, saveScrollPosition);
20290						
20291						loadPrefs(settings, [ 'audioClipVolume' ], 'lb_pref');
20292						
20293						// Passing along lightbox defaults
20294						passDefaults(settings, $.fn.lightbox.defaults, 'indexName,level,resPath,useOriginals,previousFoldersLast,previousFolderPath,nextFoldersFirst,nextFolderPath,extraSizes,hiDpiThumbs,hiDpiImages,audioClipVolume,lightboxFullscreen,lightboxFullscreenOnlyMobiles,slideshowFullscreen,rightClickProtect,enableKeyboard,mouseWheelAction,fotomoto,useRating,visitorRating,lbableClass');
20295						passDefaults(settings.lightbox, $.fn.lightbox.defaults);
20296						
20297						// Creating new Album instance
20298						initAlbum();
20299			
20300						// Right click protection on folders, slider
20301						if (settings.rightClickProtect) {
20302							avoidRightClick();
20303						}
20304						
20305						// Background music
20306						prepareBackgroundMusic($('[data-audioPlayer]'));
20307						
20308						// Hash change listener
20309						$window.on('hashchange.' + ns, stateChange);
20310						
20311						// Focusing overlay
20312						main.on('overlayReady.' + ns, function(e, o) {
20313								moveInView(o);
20314							});
20315						
20316						// Menu button wiring (small screens)
20317						$('[data-topnav-toggle]').on({
20318								'click': 		function() {
20319														$body.toggleClass('menu-on');
20320														return false;
20321													},
20322								'selectstart':	function(e) {
20323														e.preventDefault();
20324														return false;
20325													}
20326							});
20327						
20328						// Scroll to top btn
20329						if (settings.scrollToTopButton) {
20330							$body.scrollToTopBtn();
20331						}
20332						
20333						if (hasFullscreen()) {
20334							
20335							// Fullscreen change listener
20336							onFullscreenChange(function(on) {
20337									$body.toggleClass(settings.fullscreenClass, on);
20338								});
20339							
20340							$('.hero .controls .gofullscreen').on('click', requestFullscreen);
20341							$('.hero .controls .exitfullscreen').on('click', exitFullscreen);
20342							
20343							if (window.innerWidth < 640) {
20344								// Change to full screen on mobiles on orientation change
20345								goFullscreenOnLandscapeChange();
20346							}
20347						}
20348					};
20349					
20350						
20351			/********************************************************
20352			 *
20353			 *					Execution starts here
20354			 *
20355			 ********************************************************/
20356				
20357			init();
20358			
20359			// Utility function for debugging
20360			this.getAlbum = function() {
20361					return album;
20362				};
20363			
20364			return this;
20365		};
20366	
20367	/********************************************************
20368	 *
20369	 *						Defaults
20370	 *
20371	 ********************************************************/
20372				
20373	$.fn.skin.defaults = {
20374			locale:							'en-US',
20375			contentClass: 					'content',
20376			contClass:						'cont',
20377			startSlideshowHook:				'.hero .start-show',
20378			hasOverlayClass:				'has-overlay',
20379			baseOverlayClass:				'base-overlay',
20380			hideOnOverlayClass:				'hide-on-overlay',
20381			overlayClass:					'overlay',
20382			overlayTitleClass:				'title',
20383			overlayHeadLevel:				3,
20384			hasImagesClass:					'has-images',
20385			noImagesClass:					'no-images',
20386			hasMapClass:					'has-map',
20387			noMapClass:						'no-map',
20388			hasShopClass:					'has-shop',
20389			noShopClass:					'no-shop',
20390			shoppableClass:					'shoppable',
20391			progressClass:					'progress',
20392			progressbarClass:				'progressbar',
20393			maxResults:						1000,
20394			lightboxClass:					'lightbox',
20395			sliderClass:					'slider',
20396			belowfoldClass:					'below-fold',
20397			foldersClass:					'folders',
20398			folderClass:					'folder',
20399			thumbnailsClass:				'thumbnails',
20400			thumbClass:						'thumb',
20401			boxClass:						'box',
20402			mapBoxHook:						'section.map,.map-cont',
20403			mapRootClass:					'map-root',
20404			tagCloudClass:					'tag-cloud',
20405			tagSearchClass:					'tag-search',
20406			thumbLayout:					'fixgrid',
20407			thumbsFill:						true,
20408			useAudioClipButton:				false,
20409			columns:						5,
20410			thumbsPerBatch:					20,
20411			thumbDims:						[ 418, 209 ],
20412			thumbGap:						6,
20413			fixedShapeThumbs:				false,
20414			justifyWrapThreshold:			0.333333,
20415			imageClass:						'image',
20416			captionClasses:					'over bottom hover',
20417			cardClass:						'card',
20418			iconThumbClass:					'iconthumb',
20419			placeholderClass:				'placeholder',
20420			folderCardClass:				'foldercard',                     
20421			mosaicClass:					'mosaic',
20422			captionPlacement:				'below',
20423			lbableClass:					'lbable',
20424			captionClass:					'caption',
20425			commentClass:					'comment',
20426			hascaptionClass:				'hascaption',
20427			selectableClass:				'selectable',
20428			selectedClass:					'checked',
20429			activeClass:					'active',
20430			preloadClass:					'preload',
20431			lazyloadClass:					'lazyload',
20432			startshowClass:					'start-btn',
20433			fullscreenClass:				'fullscreen-on',
20434			panoClass:						'pano',
20435			selectBoxClass:					'select-box',
20436			selectAllClass:					'select-all',
20437			selectNoneClass:				'select-none',
20438			keepSelectedClass:				'keep-selected',
20439			addSelectedClass:				'add-selected',
20440			scrollCheckInterval:			120,
20441			indexName:						'index.html',
20442			folderImageFileName:			'folderimage.jpg',
20443			folderThumbFileName:			'folderthumb.jpg',
20444			audioPoster:					'audio.poster.png',
20445			videoPoster:					'video.poster.png',
20446			dateFormat:						'M/d/yyyy',
20447			audioClipVolume:				0.75,
20448			lightboxFullscreen:				false,
20449			lightboxFullscreenOnlyMobiles:	false,
20450			slideshowFullscreen:			false,
20451			rightClickProtect: 				false,
20452			useRating:						false,
20453			jalbumRating:					false,
20454			visitorRating:					false,
20455			showEmptyFilters:				false,
20456			enableKeyboard:					true,
20457			videoAuto:						true,
20458			linkToFolder:					true,
20459			useOriginals:					false,
20460			scrollToTopButton:				true
20461		};
20462		
20463})(jQuery, jQuery(window), jQuery(document));

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.