PageSourceSearch

https://www.fervere.it/wp-content/themes/fervere/nanogallery/jquery.nanogallery2.core.js?ver=7.1.2

js fervere.it collected 2026-10-08 21:03:49 UTC 569,963 bytes, 15,964 lines download raw bytes

vendor: 60,459 bytes, lines 1-1453
1/*!
2 * @preserve nanogallery2 - javascript photo / video gallery and lightbox
3 * Homepage: http://nanogallery2.nanostudio.org
4 * Sources:  https://github.com/nanostudio-org/nanogallery2
5 *
6 * License:  GPLv3 and commercial licence
7 * 
8 * Requirements:
9 *  - jQuery (http://www.jquery.com) - version >= 1.7.1
10 *
11 * Embeded components:
12 *  - shifty (https://github.com/jeremyckahn/shifty)
13 *  - imagesloaded (https://github.com/desandro/imagesloaded)
14 *  - hammer.js (http://hammerjs.github.io/)
15 *  - screenfull.js (https://github.com/sindresorhus/screenfull.js)
16 * Tools:
17 *  - webfont generated with http://fontello.com - mainly based on Font Awesome Copyright (C) 2012 by Dave Gandy (http://fontawesome.io/)
18 *  - ICO online converter: https://iconverticons.com/online/
19 */ 
20
21
22 
23// ###########################################
24// ##### nanogallery2 as a JQUERY PLUGIN #####
25// ###########################################
26
27
28
29// Expose plugin as an AMD module if AMD loader is present:
30(function (factory) {
31    "use strict";
32    if (typeof define === 'function' && define.amd) {
33        // AMD. Register as an anonymous module.
34        // define('nanogallery2', ['jquery'], factory);
35        define(['jquery'], factory);
36    } else if (typeof exports === 'object' && typeof require === 'function') {
37        // Browserify
38        factory(require('jquery'));
39    } else {
40        // Browser globals
41        factory(jQuery);
42    }
43}(function ($) {
44// ;(function ($) {
45  "use strict";
46
47  //##### TOOLS/HELPERS ####
48
49  // Convert color to RGB/RGBA
50  function ColorHelperToRGB( color ) {
51    var obj = document.getElementById('ngyColorHelperToRGB');
52    if (obj === null) {
53      obj = document.createElement('div');
54      obj.id = "ngyColorHelperToRGB";
55      obj.style.cssText = 'display: none; color:' + color + ';';
56      document.body.appendChild(obj);
57    }
58    
59    var rgb = getComputedStyle(obj).color;
60
61    // to get HEX value:
62    // var rgb = getComputedStyle(obj).color.match(/\d+/g);
63    // var r = parseInt(rgb[0]).toString(16);
64    // var g = parseInt(rgb[1]).toString(16);
65    // var b = parseInt(rgb[2]).toString(16);
66    // var hex = '#' + r + g + b;
67
68    return rgb;
69  }
70
71  
72  // ##### helper for color handling
73  // - normalise RGB/RGBA/HEX format
74  // - lighten/darken color
75  // Inspired by:          
76  // https://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors
77  // http://www.pimptrizkit.com/?t=20%20Shades
78  function ShadeBlendConvert (p, from, to) {
79    var rgba='';
80    if( from.toUpperCase().substring(0,5) == 'RGBA(' ) {
81      rgba='a';
82      from='rgb('+from.substring(5);
83    }
84
85    if(typeof(p)!="number"||p<-1||p>1||typeof(from)!="string"||(from[0]!='r'&&from[0]!='#')||(typeof(to)!="string"&&typeof(to)!="undefined"))return null;
86    //if(!this.sbcRip)this.sbcRip=function(d){
87    function sbcRip(d){
88      var l=d.length,RGB=new Object();
89      if(l>9){
90        d=d.split(",");
91        if(d.length<3||d.length>4)return null;
92        RGB[0]=i(d[0].slice(4)),RGB[1]=i(d[1]),RGB[2]=i(d[2]),RGB[3]=d[3]?parseFloat(d[3]):-1;
93      }else{
94        if(l==8||l==6||l<4)return null;
95        if(l<6)d="#"+d[1]+d[1]+d[2]+d[2]+d[3]+d[3]+(l>4?d[4]+""+d[4]:"");
96        d=i(d.slice(1),16),RGB[0]=d>>16&255,RGB[1]=d>>8&255,RGB[2]=d&255,RGB[3]=l==9||l==5?r(((d>>24&255)/255)*10000)/10000:-1;
97      }
98      return RGB;
99    }
100    var i=parseInt,r=Math.round,h=from.length>9,h=typeof(to)=="string"?to.length>9?true:to=="c"?!h:false:h,b=p<0,p=b?p*-1:p,to=to&&to!="c"?to:b?"#000000":"#FFFFFF",f=sbcRip(from),t=sbcRip(to);
101    if(!f||!t)return null;
102    if(h)return "rgb"+rgba+"("+r((t[0]-f[0])*p+f[0])+","+r((t[1]-f[1])*p+f[1])+","+r((t[2]-f[2])*p+f[2])+(f[3]<0&&t[3]<0?")":","+(f[3]>-1&&t[3]>-1?r(((t[3]-f[3])*p+f[3])*10000)/10000:t[3]<0?f[3]:t[3])+")");
103    else return "#"+(0x100000000+(f[3]>-1&&t[3]>-1?r(((t[3]-f[3])*p+f[3])*255):t[3]>-1?r(t[3]*255):f[3]>-1?r(f[3]*255):255)*0x1000000+r((t[0]-f[0])*p+f[0])*0x10000+r((t[1]-f[1])*p+f[1])*0x100+r((t[2]-f[2])*p+f[2])).toString(16).slice(f[3]>-1||t[3]>-1?1:3);
104  }
105  
106  
107  // ##### clone a javascript object
108  function cloneJSObject( obj ) {
109    if (obj === null || typeof obj !== 'object') {
110      return obj;
111    }
112
113    var temp = obj.constructor(); // give temp the original obj's constructor
114    for (var key in obj) {
115        temp[key] = cloneJSObject(obj[key]);
116    }
117    return temp;
118  }
119  
120  // get viewport coordinates and size
121  function getViewport() {
122    var $win = jQuery(window);
123    return {
124      l: $win.scrollLeft(),
125      t: $win.scrollTop(),
126      w: $win.width(),
127      h: $win.height()
128    }
129  }
130
131
132  // Check if element is in viewport
133  // avoid if possible (performance issue)
134  /*
135  function inViewport( $elt, threshold ) {
136    var wp = getViewport(),
137    eltOS = $elt.offset(),
138    th = $elt.outerHeight(true),
139    tw = $elt.outerWidth(true);
140    if( eltOS.top >= (wp.t - threshold) 
141      && (eltOS.top + th) <= (wp.t + wp.h + threshold)
142      && eltOS.left >= (wp.l - threshold) 
143      && (eltOS.left + tw) <= (wp.l + wp.w + threshold) ) {
144      return true;
145    }
146    else {
147      return false;
148    }
149  }
150  */
151
152
153  // Check if whole element is in ViewPort
154  // avoid if possible (performance issue)
155  function inViewportVert( $elt, threshold ) {
156    var wp = getViewport(),
157    eltOS = $elt.offset(),
158    th = $elt.outerHeight(true);
159    //var tw=$elt.outerWidth(true);
160
161    if( wp.t == 0 && (eltOS.top) <= (wp.t + wp.h ) ) { return true; }
162
163    if( eltOS.top >= wp.t && (eltOS.top + th) <= (wp.t + wp.h - threshold) ) {
164        return true;
165    }
166    else {
167      return false;
168    }
169  }
170  // Check if top of the element is in ViewPort
171  function topInViewportVert( $elt, threshold ) {
172    var wp = getViewport(),
173    eltOS = $elt.offset();
174
175    if( eltOS.top >= wp.t && eltOS.top <= (wp.t + wp.h - threshold) ) {
176        return true;
177    }
178    else {
179      return false;
180    }
181  }
182
183
184  // set z-index to display 2 elements on top of all others
185  // function set2ElementsOnTop( start, elt1, elt2 ) {
186    // var highest_index = 0;
187    // if( start=='' ) { start= '*'; }
188    // jQuery(start).each(function() {
189      // var cur = parseInt(jQuery(this).css('z-index'));
190      // highest_index = cur > highest_index ? cur : highest_index;
191    // });
192    // highest_index++;
193    // jQuery(elt2).css('z-index',highest_index+1);
194    // jQuery(elt1).css('z-index',highest_index);
195  // }
196
197  // set z-index to display element on top of all others
198  function setElementOnTop( start, elt ) {
199    var highest_index = 0;
200    if( start == '' ) { start = '*'; }
201    jQuery(start).each(function() {
202      var cur = parseInt(jQuery(this).css('z-index'));
203      highest_index = cur > highest_index ? cur : highest_index;
204    });
205    highest_index++;
206    jQuery(elt).css('z-index',highest_index);
207  }
208  
209  // return the real type of the object
210  var toType = function( obj ) {
211    // by Angus Croll - http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/
212    return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase()
213  };    
214    
215
216  $.nanogallery2 = function (elt, options) {
217    // To avoid scope issues, use '_this' instead of 'this'
218    // to reference this class from internal events and functions.
219    var _this = this;
220
221    // Access to jQuery and DOM versions of element
222    _this.$e  = jQuery(elt);
223    _this.e   = elt;
224
225    // Add a reverse reference to the DOM object
226    _this.$e.data('nanogallery2data', _this);
227
228    _this.init = function () {
229  
230      // define these global objects only once per HTML page
231      if (typeof window.NGY2Item === 'undefined') {
232  
233        window.NGY2Tools = (function () {
234
235          function NGY2Tools() {
236            var nextId = 1;                   // private static --> all instances
237          }
238
239          // check album name - albumList/blockList/allowList
240          NGY2Tools.FilterAlbumName = function( title, ID ) {
241            var s = title.toUpperCase();
242            if( this.albumList.length > 0 ) {
243              for( var j=0; j < this.albumList.length; j++) {
244                if( s === this.albumList[j].toUpperCase() || ID === this.albumList[j] ) {
245                  return true;
246                }
247              }
248            }
249            else {
250              var found = false;
251              if( this.allowList !== null ) {
252                //allowList : authorize only album cointaining one of the specified keyword in the title
253                for( var j = 0; j < this.allowList.length; j++) {
254                  if( s.indexOf(this.allowList[j]) !== -1 ) {
255                    found = true;
256                  }
257                }
258                if( !found ) { return false; }
259              }
260
261
262              if( this.blockList !== null ) {
263                //blockList : ignore album cointaining one of the specified keyword in the title
264                for( var j = 0; j < this.blockList.length; j++) {
265                  if( s.indexOf(this.blockList[j]) !== -1 ) { 
266                    return false;
267                  }
268                }
269              }
270              return true;
271            }
272          };
273
274
275          /** @function nanoAlert */
276          /* Display an alert message in a specific element */
277          NGY2Tools.NanoAlert = function(context, msg, verbose) {
278            NGY2Tools.NanoConsoleLog.call(context, msg);
279            if( context.$E.conConsole != null ) {
280              context.$E.conConsole.css({visibility: 'visible', minHeight: '100px'});
281              if( verbose == false ) {
282                context.$E.conConsole.append('<p>' + msg + '</p>');
283              }
284              else {
285                context.$E.conConsole.append('<p>nanogallery2: '+ msg + ' [' + context.baseEltID + ']</p>');
286              }
287            }
288          };
289          
290  
291          /** @function NanoConsoleLog */
292          /* write message to the browser console */
293          NGY2Tools.NanoConsoleLog = function(context, msg) {
294            if (window.console) { console.log('nanogallery2: ' + msg + ' [' + context.baseEltID + ']'); }
295            // debugger;
296          };
297          
298
299          /** @function PreloaderDisplay() */
300          /* Display/hide preloader */
301          NGY2Tools.PreloaderDisplay = function(display) {
302            if( display === true ) {
303              // loading bar at the top of the gallery
304              this.$E.conLoadingB.removeClass('nanoGalleryLBarOff').addClass('nanoGalleryLBar');
305              // spinner over album thumbnail
306              if( this.GOM.albumIdxLoading != undefined && this.GOM.albumIdxLoading != -1 ) {
307                let item = this.I[this.GOM.albumIdxLoading];
308                item.$Elts['.nGY2TnImg'].addClass('nGY2GThumbnailLoaderDisplayed');
309              }
310            }
311            else {
312              // loading bar at the top of the gallery
313              this.$E.conLoadingB.removeClass('nanoGalleryLBar').addClass('nanoGalleryLBarOff');
314              // spinner over album thumbnail
315              if( this.GOM.albumIdxLoading != undefined && this.GOM.albumIdxLoading != -1 ) {
316                let item = this.I[this.GOM.albumIdxLoading];
317                item.$Elts['.nGY2TnImg'].removeClass('nGY2GThumbnailLoaderDisplayed');
318              }
319            }
320          };
321
322          // Scrambles the elements of an array
323					//+ Jonas Raoni Soares Silva
324          //@ http://jsfromhell.com/array/shuffle [v1.0]
325          NGY2Tools.AreaShuffle = function (o) {
326            for (var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
327            return o;
328          };
329          
330          /** @function GetImageTitleFromURL() */
331          /* retrieve filemane */
332          NGY2Tools.GetImageTitleFromURL = function( imageURL ) {
333            if( this.O.thumbnailLabel.get('title') == '%filename' ) {
334              return (imageURL.split('/').pop()).replace('_',' ');
335            }
336            
337            if( this.O.thumbnailLabel.get('title') == '%filenameNoExt' ) {
338              var s=imageURL.split('/').pop();
339              return (s.split('.').shift()).replace('_',' ');
340            }
341            // return imageURL;
342            return '';
343          };
344          
345
346          /** @function AlbumPostProcess() */
347          /* post process one album based on plugin general parameters  --> sorting/maxItems*/
348          NGY2Tools.AlbumPostProcess = function(albumID) {
349
350            // this function can probably be optimized....
351          
352            var sortOrder = this.gallerySorting[this.GOM.curNavLevel];
353            var maxItems = this.galleryMaxItems[this.GOM.curNavLevel];
354
355            if( sortOrder != '' || maxItems > 0 ) {
356            
357              // copy album's items to a new array
358              var currentAlbum = this.I.filter( function( obj ) {
359                return( obj.albumID == albumID && obj.kind != 'albumUp' );
360              });
361        
362              // sorting options
363              switch( sortOrder ) {
364                case 'RANDOM':
365                  currentAlbum = NGY2Tools.AreaShuffle(currentAlbum);
366                  break;
367                case 'REVERSED':
368                  currentAlbum = currentAlbum.reverse();
369                  break;
370                case 'TITLEASC':
371                  currentAlbum.sort(function (a, b) {
372                    return( (a.title.toUpperCase() < b.title.toUpperCase()) ? -1 : ((a.title.toUpperCase() > b.title.toUpperCase()) ? 1 : 0) );
373                  });
374                  break;
375                case 'TITLEDESC':
376                  currentAlbum.sort(function (a, b) {
377                    return( (a.title.toUpperCase() > b.title.toUpperCase()) ? -1 : ((a.title.toUpperCase() < b.title.toUpperCase()) ? 1 : 0) );
378                  });
379                  break;
380              }     
381
382              // max Items
383              if( maxItems > 0 && currentAlbum.length > maxItems ) {
384                currentAlbum.splice(maxItems - 1, currentAlbum.length-maxItems );
385              }
386              
387              // remove the albums's items from the global items array
388              this.I.ngy2removeIf( function( obj ) {
389                return( obj.albumID == albumID && obj.kind != 'albumUp' );
390              });
391              
392              // add the sorted items back to the album
393              this.I.push.apply(this.I, currentAlbum);
394
395            }
396          };
397          
398          return NGY2Tools;
399        })(); 
400
401        // ====================
402        // ===== NGY2Item =====
403        // ====================
404        window.NGY2Item = (function() {
405          var nextId = 1;                   // private static --> all instances
406
407          // constructor
408          function NGY2Item( itemID ) {
409          //window.NGY2Item = function( itemID ) {
410            var ID = 0;                     // private
411
412            // public (this instance only)
413            if( itemID === undefined || itemID === null ) {
414              ID = nextId++;
415            }
416            else {
417              ID = itemID;
418            }
419            this.GetID = function () { return ID; };
420            
421            // public
422            this.kind =                 '';       // 'image', 'album' or 'albumUp'
423            this.mediaKind =            'img';    // 'img', 'iframe', 'video'
424            this.mediaMarkup =          '';
425            this.G =                    null;     // pointer to global instance
426            this.title =                '';       // image title
427            this.description =          '';       // image description
428            this.albumID =              0;        // ID of the parent album
429            this.src =                  '';       // full sized image URL
430            this.width =                0;        // image width
431            this.height =               0;        // image height
432            this.destinationURL =       '';       // thumbnail destination URL --> open URL instead of displaying image
433            this.downloadURL =          '';       // thumbnail download URL --> specify the image for download button
434            this.author =               '';       // image/album author
435            this.left =                 0;        // store position to animate from old to new
436            this.top =                  0;
437            this.width =                0;        // store size to avoid setting width/height if not required
438            this.height =               0;
439            this.resizedContentWidth=   0;        // store size of content (image) to avoid setting width/height if not required
440            this.resizedContentHeight=  0;
441            this.thumbs = {                       // URLs and sizes for user defined
442              url:    { l1: { xs: '', sm:'', me: '', la: '', xl: '' }, lN: { xs: '', sm: '', me: '', la:'', xl: '' } },
443              width:  { l1: { xs: 0,  sm: 0, me: 0,  la: 0 , xl: 0  }, lN: { xs: 0 , sm: 0,  me: 0,  la: 0, xl: 0  } },
444              height: { l1: { xs: 0,  sm: 0, me: 0,  la: 0 , xl: 0  }, lN: { xs: 0,  sm: 0,  me: 0,  la: 0, xl: 0  } }
445            };
446            this.thumbnailImgRevealed   = false;  // thumbnail image already revealed
447            this.imageDominantColors    = null;   // base64 GIF
448            this.imageDominantColor     = null;   // HEX RGB
449            this.featured =             false;    // featured element
450            this.flickrThumbSizes =     {};       // store URLs for all available thumbnail sizes (flickr)
451            this.picasaThumbs =         null;     // store URLs and sizes
452            this.hovered =              false;    // is the thumbnail currently hovered?
453            this.hoverInitDone =        false;
454            this.contentIsLoaded =      false;    // album: are items already loaded?
455            this.contentLength =        0;        // album: number of items (real number of items in memory)
456            this.numberItems =          0;        // album: number of items (value returned by data source)
457            this.mediaNumber =          0;        // media number in the album
458            this.mediaCounter =         0;        // number of medias in an album
459            this.eltTransform =         [];       // store the CSS transformations
460            this.eltFilter =            [];       // store the CSS filters
461            this.eltEffect =            [];       // store data about hover effects animations
462            this.paginationLastPage =   0;        // for albums
463            this.paginationLastWidth =  0;        // for albums
464            this.customData =           {};
465            this.selected =             false;
466            this.imageWidth =           0;        // image natural (real) width
467            this.imageHeight =          0;        // image natural (real) height
468            this.$elt =                 null;     // pointer to the corresponding DOM element
469            this.$Elts =                [];       // cached pointers to the thumbnail content -> to avoid jQuery().find()
470            this.tags =                 [];       // list of tags of the current item
471            this.albumTagList =         [];       // list of all the tags of the items contained in the current album
472            this.albumTagListSel =      [];       // list of currently selected tags (only for albums)
473            this.exif = { exposure: '', flash: '', focallength: '', fstop: '', iso: '', model: '', time: '', location: ''};
474            this.deleted =              false;    // item is deleted -> do not display anymore
475            this.rotationAngle =        0;        // image display rotation angle
476          }
477
478          // public static
479          
480          NGY2Item.Get = function( instance, ID ) {
481            var l = instance.I.length;
482            for( var i = 0; i < l; i++ ) {
483              if( instance.I[i].GetID() == ID ) {
484                return instance.I[i];
485              }
486            }
487            return null;
488          };
489            
490          NGY2Item.GetIdx = function( instance, ID ) {
491            var l = instance.I.length;
492            for( var i = 0; i < l; i++ ) {
493              if( instance.I[i].GetID() == ID ) {
494                return i;
495              }
496            }
497            return -1;
498          };
499          
500          // create new item (image, album or albumUp)
501          NGY2Item.New = function( instance, title, description, ID, albumID, kind, tags ) {
502            var album = NGY2Item.Get( instance, albumID );
503            
504            // title translation
505            if( instance.O.titleTranslationMap !== null ) {
506              let obj = instance.O.titleTranslationMap.find(o => o.title === title);
507              if( obj !== undefined ) {
508                title = obj.replace;
509              }
510            }
511            
512            
513            if( albumID != -1 && albumID != 0 && title !='image gallery by nanogallery2 [build]'  ) {
514              if( instance.O.thumbnailLevelUp && album.getContentLength(false) == 0 && instance.O.album == '' ) {
515                // add navigation thumbnail (album up)
516                let item = new NGY2Item('0');
517                instance.I.push( item );
518                album.contentLength += 1;
519                item.title = 'UP';
520                item.albumID = albumID;
521                item.kind = 'albumUp';
522                item.G = instance;
523
524                jQuery.extend( true, item.thumbs.width, instance.tn.defaultSize.width);
525                jQuery.extend( true, item.thumbs.height, instance.tn.defaultSize.height);
526              }
527            }
528            
529            var item = NGY2Item.Get(instance, ID);
530            if( item === null ){
531              // create a new item (otherwise, just update the existing one)
532              item = new NGY2Item(ID);
533              instance.I.push(item);
534              if( albumID != -1 && title !='image gallery by nanogallery2 [build]' ) {
535                album.contentLength+=1;
536              }
537            }
538            item.G = instance;
539
540            item.albumID = albumID;
541            item.kind = kind;
542            if( kind == 'image' ) {
543              album.mediaCounter += 1;
544              item.mediaNumber = album.mediaCounter;
545            }
546
547            // check keyword to find features images/albums
548            var kw = instance.O.thumbnailFeaturedKeyword;
549            if( kw != '' ) {
550              // check if item featured based on a keyword in the title or in the description
551              kw = kw.toUpperCase();
552              var p = title.toUpperCase().indexOf(kw);
553              if( p > -1) {
554                item.featured = true;
555                // remove keyword case unsensitive
556                title = title.substring(0, p) + title.substring(p+kw.length, title.length);
557              }
558              p = description.toUpperCase().indexOf(kw);
559              if( p > -1) {
560                item.featured=true;
561                // remove keyword case unsensitive
562                description=description.substring(0, p) + description.substring(p + kw.length, description.length);
563              }
564            }
565            
566            // TAGS 
567            // if( instance.galleryFilterTags.Get() != false ) {
568              // if( instance.galleryFilterTags.Get() == true ) {
569                // if( tags != '' && tags != undefined ) {
570                  // use set tags
571                  // item.setTags(tags.split(' '));
572                // }
573              // }
574              // else {
575                // extract tags starting with # (in title)
576              if( typeof instance.galleryFilterTags.Get() == 'string' ) {
577                switch( instance.galleryFilterTags.Get().toUpperCase() ) {
578                  case 'TITLE': {
579                      let re = /(?:^|\W)#(\w+)(?!\w)/g, match, matches = [];
580                      // let tags = "";
581                      while (match = re.exec(title)) {
582                        matches.push(match[1].replace(/^\s*|\s*$/, ''));   //trim trailing/leading whitespace
583                      }
584                      item.setTags(matches);  //tags;
585                      title = title.split('#').join('');   //replaceall
586                      break;
587                    }
588                  case 'DESCRIPTION': {
589                      let re = /(?:^|\W)#(\w+)(?!\w)/g, match2, matches2 = [];
590                      // let tags = "";
591                      while (match2 = re.exec(description)) {
592                        matches2.push(match2[1].replace(/^\s*|\s*$/, ''));   //trim trailing/leading whitespace
593                      }
594                      item.setTags(matches2);  //tags;
595                      description = description.split('#').join('');   //replaceall
596                      break;
597                    }
598                }
599              }
600                else {
601                  if( tags != '' && tags != undefined ) {
602                    // use set tags
603                    item.setTags(tags.split(' '));
604                  }
605                }
606              // }
607            // }
608            
609            // set (maybe modified) fields title and description
610            item.title = escapeHtml(instance, title);
611            item.description = escapeHtml(instance, description);
612            return item;
613          };
614          
615          
616          // removes logically current item
617          NGY2Item.prototype.delete = function( ) {
618            this.deleted = true;
619            
620            // update content length of parent album
621            this.G.I[NGY2Item.GetIdx(this.G, this.albumID)].contentLength--;
622            this.G.I[NGY2Item.GetIdx(this.G, this.albumID)].numberItems--;
623            
624            // check if in GOM and removes it
625            var nbTn = this.G.GOM.items.length;
626            var ID = this.GetID();
627            var foundIdx = -1;
628            var foundGOMidx = -1;
629            for( var i = 0; i < nbTn ; i++ ) {
630              var curTn = this.G.GOM.items[i];
631              var item=this.G.I[curTn.thumbnailIdx];
632              if( item.GetID() == ID ) {
633                // FOUND
634                if( !curTn.neverDisplayed ) {
635                  foundIdx = curTn.thumbnailIdx;
636                  foundGOMidx = i;
637                }
638              }
639              else {
640                if( foundIdx != -1 ) {
641                  if( !curTn.neverDisplayed ) {
642                    // update index value
643                    item.$getElt('.nGY2GThumbnail').data('index', i-1);
644                    item.$getElt('.nGY2GThumbnailImg').data('index', i-1);
645                  }
646                }
647              }
648            }
649            if( foundIdx != -1 ) {
650              // delete item in GOM and delete thumbnail
651              var G = this.G;
652              if( this.selected == true ) {
653                this.selected = false;
654                G.GOM.nbSelected--;    // update the global counter
655              }
656              if( G.I[foundIdx].$elt !== null ) {
657                G.I[foundIdx].$elt.remove();      // delete thumbnail DOM object
658              }
659              G.GOM.items.splice(foundGOMidx, 1);   // delete in GOM
660              if( G.GOM.lastDisplayedIdx != -1 ) {
661                G.GOM.lastDisplayedIdx -= 1;
662              }
663            }
664            
665            // TODO: update media-number of the other item in the same album
666          }
667
668          NGY2Item.prototype.addToGOM = function( ) {
669            // retrieve index
670            var ID = this.GetID();
671            var l = this.G.I.length;
672            for( var idx = 0; idx < l; idx++ ) {
673              var item = this.G.I[idx];
674              if( item.GetID() == ID ) {
675                var w = item.thumbImg().width;
676                var h = item.thumbImg().height;
677                // set default size if required
678                if( h == 0 ) {
679                  h = this.G.tn.defaultSize.getHeight();
680                }
681                if( w == 0 ) {
682                  w = this.G.tn.defaultSize.getWidth();
683                }
684                // add to GOM -> will be displayed on next refresh/resize
685                var tn = new this.G.GOM.GTn(idx, w, h);
686                this.G.GOM.items.push(tn);
687                break;
688              }
689            }
690            
691          }
692          
693          
694          // function to avoid XSS issue - Cross Site Scripting
695          // original: https://github.com/janl/mustache.js/blob/master/mustache.js#L55
696          var entityMap = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', '/': '&#x2F;', '`': '&#x60;', '=': '&#x3D;' };
697          function escapeHtml (instance, string) {
698            if( instance.O.allowHTMLinData == true ) {
699              return string;
700            }
701            else {
702              return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
703                return entityMap[s];
704              });
705            }
706          }
707          
708          
709          NGY2Item.get_nextId = function () {
710            return nextId;
711          };
712
713          //=== public (shared across instances)
714
715          //--- cached sub elements
716          NGY2Item.prototype.$getElt = function( elt, forceRefresh ) {
717            if( this.$elt == null ) { return null; }
718            if( this.$Elts[elt] !== undefined && !forceRefresh == true ) {
719              return this.$Elts[elt];
720            }
721            else {
722              if( elt == '.nGY2GThumbnail' ) {
723                this.$Elts[elt]=this.$elt;
724              }
725              else {
726                this.$Elts[elt]=this.$elt.find(elt);
727              }
728              return this.$Elts[elt];
729            }
730          };
731          
732          // remove one element (in DOM and in cache)
733          NGY2Item.prototype.removeElt = function( elt ) {
734            if( this.$elt == null ) { return; }
735            if( this.$Elts[elt] == undefined) { return; }
736            this.$Elts[elt].remove();
737            var index = this.$Elts.indexOf(elt);
738            this.$Elts.splice(index, 1);
739          };
740
741          //--- returns the album containing the item
742          NGY2Item.prototype.album = function() {
743            return this.G.I[NGY2Item.GetIdx(this.G, this.albumID)];
744          };
745          
746          //--- viewer - transition can be disabled per media kind - returns true if current media supports transition (swipe)
747          NGY2Item.prototype.mediaTransition = function( ) {
748            if( this.G.O.viewerTransitionMediaKind.indexOf( this.mediaKind ) > -1 ) {
749              return true;
750            }
751            return false;
752          };
753
754          //--- set one image (url and size)
755          NGY2Item.prototype.imageSet = function( src, w, h ) {
756            this.src = src;
757            this.width = w;
758            this.height = h;
759          };
760          
761          //--- set one thumbnail (url and size) - screenSize and level are optional
762          NGY2Item.prototype.thumbSet = function( src, w, h, screenSize, level ) {
763            var lst=['xs','sm','me','la','xl'];
764            if( typeof screenSize === 'undefined' || screenSize == '' || screenSize == null ) {
765              for( var i=0; i< lst.length; i++ ) {
766                if( typeof level === 'undefined' || level == '' ) {
767                  this.thumbs.url.l1[lst[i]]=src;
768                  this.thumbs.height.l1[lst[i]]=h;
769                  this.thumbs.width.l1[lst[i]]=w;
770                  this.thumbs.url.lN[lst[i]]=src;
771                  this.thumbs.height.lN[lst[i]]=h;
772                  this.thumbs.width.lN[lst[i]]=w;
773                }
774                else {
775                  this.thumbs.url[level][lst[i]]=src;
776                  this.thumbs.height[level][lst[i]]=h;
777                  this.thumbs.width[level][lst[i]]=w;
778                }
779              }
780            }
781            else {
782              if( typeof level === 'undefined' || level == '' || level == null ) {
783                this.thumbs.url.l1[screenSize]=src;
784                this.thumbs.height.l1[screenSize]=h;
785                this.thumbs.width.l1[screenSize]=w;
786                this.thumbs.url.lN[screenSize]=src;
787                this.thumbs.height.lN[screenSize]=h;
788                this.thumbs.width.lN[screenSize]=w;
789              }
790              else {
791                this.thumbs.url[level][screenSize]=src;
792                this.thumbs.height[level][screenSize]=h;
793                this.thumbs.width[level][screenSize]=w;
794              }
795            }
796          
797            for( var i=0; i< lst.length; i++ ) {
798              this.thumbs.height.l1[lst[i]]=h;
799            }
800            for( var i=0; i< lst.length; i++ ) {
801              if( this.G.tn.settings.height.lN[lst[i]] == this.G.tn.settings.getH() && this.G.tn.settings.width.l1[lst[i]] == this.G.tn.settings.getW() ) {
802                this.thumbs.height.lN[lst[i]]=h;
803              }
804            }
805          };
806
807          //--- set thumbnail image real height for current level/resolution, and for all others level/resolutions having the same settings
808          NGY2Item.prototype.thumbSetImgHeight = function( h ) {              
809            var lst=['xs','sm','me','la','xl'];
810            for( var i=0; i< lst.length; i++ ) {
811              if( this.G.tn.settings.height.l1[lst[i]] == this.G.tn.settings.getH() && this.G.tn.settings.width.l1[lst[i]] == this.G.tn.settings.getW() ) {
812                this.thumbs.height.l1[lst[i]]=h;
813              }
814            }
815            for( var i=0; i< lst.length; i++ ) {
816              if( this.G.tn.settings.height.lN[lst[i]] == this.G.tn.settings.getH() && this.G.tn.settings.width.l1[lst[i]] == this.G.tn.settings.getW() ) {
817                this.thumbs.height.lN[lst[i]]=h;
818              }
819            }
820          };
821
822          //--- set thumbnail image real width for current level/resolution, and for all others level/resolutions having the same settings
823          NGY2Item.prototype.thumbSetImgWidth = function( w ) {              
824            var lst=['xs','sm','me','la','xl'];
825            for( var i=0; i< lst.length; i++ ) {
826              if( this.G.tn.settings.height.l1[lst[i]] == this.G.tn.settings.getH() && this.G.tn.settings.width.l1[lst[i]] == this.G.tn.settings.getW() ) {
827                this.thumbs.width.l1[lst[i]]=w;
828              }
829            }
830            for( var i=0; i< lst.length; i++ ) {
831              if( this.G.tn.settings.height.lN[lst[i]] == this.G.tn.settings.getH() && this.G.tn.settings.width.l1[lst[i]] == this.G.tn.settings.getW() ) {
832                this.thumbs.width.lN[lst[i]]=w;
833              }
834            }
835          };
836          
837          //--- Returns Thumbnail image (depending of the screen resolution)
838          NGY2Item.prototype.thumbImg = function () {   
839            var tnImg = { src: '', width: 0, height: 0 };
840
841            if( this.title == 'image gallery by nanogallery2 [build]' ) {
842              tnImg.src = this.G.emptyGif;
843              tnImg.url = this.G.emptyGif;
844              return tnImg;
845            }
846            tnImg.src = this.thumbs.url[this.G.GOM.curNavLevel][this.G.GOM.curWidth];
847            tnImg.width = this.thumbs.width[this.G.GOM.curNavLevel][this.G.GOM.curWidth];
848            tnImg.height = this.thumbs.height[this.G.GOM.curNavLevel][this.G.GOM.curWidth];
849            return tnImg;
850          };
851          
852          //--- Set tags to items and add these tags to the album
853          NGY2Item.prototype.setTags = function( tags ) {              
854          if( tags.length > 0 ) {
855              this.tags = tags;
856              var lstTags = this.album().albumTagList;
857              for( var i = 0; i < tags.length; i++ ) {
858                var tfound = false;
859                for( var j = 0; j < lstTags.length; j++ ) {
860                  if( tags[i].toUpperCase() == lstTags[j].toUpperCase() ) {
861                    tfound = true;
862                  }
863                }
864                if( tfound == false) {
865                  this.album().albumTagList.push(tags[i])
866                  // this.album().albumTagListSel.push(tags[i])
867                }
868              }
869            }
870          };
871          
872          //--- check if 1 of current item's tags is selected (tag filter)
873          NGY2Item.prototype.checkTagFilter = function() {
874            if( this.G.galleryFilterTags.Get() != false && this.album().albumTagList.length > 0 ) {
875              if( this.G.O.thumbnailLevelUp && this.kind == 'albumUp' ) {
876                return true;
877              }
878              var found = false;
879              var lstTags = this.album().albumTagListSel;
880              if( lstTags.length == 0 ) {       
881                // no tag is selected -> display all items
882                return true;
883              }
884              for( var i = 0; i < this.tags.length; i++ ) {
885                for( var j = 0; j < lstTags.length; j++ ) {
886                  if( this.tags[i].toUpperCase() == lstTags[j].toUpperCase() ) {
887                    found = true;
888                    break;
889                  }
890                }
891              }
892              return found;
893            }
894            else
895              return true;
896          };
897          
898          //--- check if 1 of current item's tags is found using API search
899          NGY2Item.prototype.isSearchTagFound = function() {
900            if( this.G.GOM.albumSearchTags == '' ) { return true; }
901            if( this.G.O.thumbnailLevelUp && this.kind == 'albumUp' ) { return true; }
902
903            //var lstTags=this.album().albumTagListSel;
904            for( var i = 0; i < this.tags.length; i++ ) {
905              if( this.tags[i].toUpperCase().indexOf( this.G.GOM.albumSearchTags ) >= 0 ) {
906                return true;
907              }
908            }
909            return false;
910          };
911          
912          //--- set the URL of the media to display in the viewer
913          //--- markup is defined for images
914          NGY2Item.prototype.setMediaURL = function( url, mediaKind ) {
915            this.src = url;
916            this.mediaKind = mediaKind;
917            if( mediaKind == 'img' ) {
918              this.mediaMarkup = '<img class="nGY2ViewerMedia" src="' + url + '" alt=" " itemprop="contentURL" draggable="false">';
919            }
920          };
921          
922          
923          //--- check if current item should be displayed
924          NGY2Item.prototype.isToDisplay = function( albumID ) {
925            return this.albumID == albumID && this.checkTagFilter() && this.isSearchFound() && this.isSearchTagFound() && this.deleted == false;
926          };
927          
928          
929          
930          //--- returns the number of items of the current album
931          //--- count using tags filter
932          NGY2Item.prototype.getContentLength = function( filterTags ) {
933            if( filterTags == false || this.albumTagList.length == 0 || this.G.galleryFilterTags.Get() == false ) {
934              return this.contentLength;
935            }
936            else {
937              var l = this.G.I.length;
938              var cnt = 0;
939              var albumID = this.GetID();
940              for( var idx = 0; idx < l; idx++ ) {
941                var item = this.G.I[idx];
942                if( item.isToDisplay(albumID) ) {
943                  cnt++;
944                }
945              }
946              return cnt;
947            }
948          };
949          
950          NGY2Item.prototype.isSearchFound = function() {
951            if( this.G.GOM.albumSearch != '' ) {
952              if( this.title.toUpperCase().indexOf( this.G.GOM.albumSearch ) == -1 ) {
953                return false;
954              }
955            }
956            return true;
957          }
958          
959          
960          //--- for future use...
961          NGY2Item.prototype.responsiveURL = function () {
962            var url = '';
963            switch(this.G.O.kind) {
964              case '':
965                url = this.src;
966                break;
967              case 'flickr':
968                url = this.src;
969                break;
970              case 'picasa':
971              case 'google':
972              case 'google2':
973              default:
974                url = this.src;
975                break;
976            }
977            return url;
978          };
979          
980          
981          //--- Reveal the thumbnail image with animation on opacity
982          NGY2Item.prototype.ThumbnailImageReveal = function () {
983
984            if( this.thumbnailImgRevealed == false ) {
985              this.thumbnailImgRevealed = true;
986              new NGTweenable().tween({
987                from:         { opacity: 0 },
988                to:           { opacity: 1 },
989                attachment:   { item: this },
990                delay:        30,
991                duration:     400,
992                easing:       'easeOutQuart',
993                step:         function (state, att) {
994                  var $e=att.item.$getElt('.nGY2TnImg');
995                  if( $e != null ) {
996                    $e.css( state );
997                  }
998                }
999              });
1000            }
1001          };
1002          
1003          
1004          // In case of thumbnails with stacks - apply a percent to a value which include a unit
1005          function ValueApplyPercent( str, percent ) {
1006            str=String(str);
1007            if( str === '0' || percent == 1 ) { return str; }
1008            var n = Number(str.replace(/[a-zA-Z]/g, ''));
1009            var ar = str.match(/([^\-0-9\.]+)/g);
1010            var a = '';
1011            if( ar != null && ar.length > 0 ) {
1012              a = ar.join();
1013            }
1014             
1015            if( isNaN(n) || n == 0 ) {
1016              return str;
1017            }
1018
1019            n = n * percent;
1020            return n + a;
1021          } 
1022          
1023          //--- 2D/3D CSS transform - apply the cached value to element
1024          NGY2Item.prototype.CSSTransformApply = function ( eltClass ) {
1025            var obj = this.eltTransform[eltClass];
1026
1027            if( eltClass == '.nGY2GThumbnail' ) {
1028              // thumbnail
1029              var nbStacks = obj.$elt.length-1;
1030              var pTranslateX = 1;
1031              var pTranslateY = 1;
1032              var pTranslateZ = 1;
1033              var pTranslate = 1;
1034              var pRotateX = 1;
1035              var pRotateY = 1;
1036              var pRotateZ = 1;
1037              var pRotate = 1;
1038              var pScale = 1;
1039              for( var n = nbStacks; n >= 0; n-- ) {
1040                // units must be given with
1041                var v = 'translateX(' + ValueApplyPercent(obj.translateX,pTranslateX) + ') translateY(' + ValueApplyPercent(obj.translateY,pTranslateY) + ') translateZ(' + ValueApplyPercent(obj.translateZ,pTranslateZ) + ') scale(' + ValueApplyPercent(obj.scale,pScale) + ') translate(' + ValueApplyPercent(obj.translate,pTranslate) + ')';
1042                if( !(this.G.IE <= 9) && !this.G.isGingerbread ) {
1043                  v += ' rotateX(' + ValueApplyPercent(obj.rotateX,pRotateX) + ') rotateY(' + ValueApplyPercent(obj.rotateY,pRotateY) + ') rotateZ(' + ValueApplyPercent(obj.rotateZ,pRotateZ) + ') rotate(' + ValueApplyPercent(obj.rotate,pRotate) + ')';
1044                }
1045                else {
1046                  v += ' rotate(' + ValueApplyPercent(obj.rotateZ,pRotateZ) + ')';
1047                }
1048                obj.$elt[n].style[this.G.CSStransformName] = v;
1049                
1050                if( nbStacks > 0 ) {
1051                  // apply a percent to the stack elements
1052                  pTranslateX -= this.G.tn.opt.Get('stacksTranslateX');
1053                  pTranslateY -= this.G.tn.opt.Get('stacksTranslateY');
1054                  pTranslateZ -= this.G.tn.opt.Get('stacksTranslateZ');
1055                  pRotateX    -= this.G.tn.opt.Get('stacksRotateX');
1056                  pRotateY    -= this.G.tn.opt.Get('stacksRotateY');
1057                  pRotateZ    -= this.G.tn.opt.Get('stacksRotateZ');
1058                  pScale      -= this.G.tn.opt.Get('stacksScale');
1059                }
1060              }
1061            }
1062            else {
1063              // thumbnail sub element
1064              if( obj.$elt != null ) {
1065                for( var n = 0; n < obj.$elt.length; n++ ) {
1066                  if( obj.$elt[n] != undefined ) {
1067                    // units must be given with
1068                    var v = 'translateX(' + obj.translateX + ') translateY(' + obj.translateY + ') translateZ(' + obj.translateZ + ') scale(' + obj.scale + ') translate(' + obj.translate + ')';
1069                    if( !(this.G.IE <= 9) && !this.G.isGingerbread ) {
1070                      v += ' rotateX(' + obj.rotateX + ') rotateY(' + obj.rotateY + ') rotateZ(' + obj.rotateZ + ') rotate(' + obj.rotate + ')';
1071                    }
1072                    else {
1073                      v += ' rotate(' + obj.rotateZ + ')';
1074                    }
1075                    obj.$elt[n].style[this.G.CSStransformName] = v;
1076                    }
1077                }
1078              }
1079            }
1080          };
1081
1082          //--- 2D/3D CSS transform - set a value in cache
1083          NGY2Item.prototype.CSSTransformSet = function ( eltClass, transform, value, forceRefresh ) {
1084            if( this.eltTransform[eltClass] == undefined ) {
1085              this.eltTransform[eltClass] = { translateX: 0, translateY: 0, translateZ: 0, rotateX: 0, rotateY: 0, rotateZ: 0, scale: 1, translate: '0px,0px', rotate: 0 };
1086              this.eltTransform[eltClass].$elt = this.$getElt(eltClass);
1087            }
1088            this.eltTransform[eltClass][transform] = value;
1089            if( forceRefresh === true ) {
1090              this.eltTransform[eltClass].$elt = this.$getElt(eltClass, true);
1091            }
1092          };
1093
1094          //--- CSS Filters - apply the cached value to element
1095          NGY2Item.prototype.CSSFilterApply = function ( eltClass ) {
1096            var obj = this.eltFilter[eltClass];
1097            var v = 'blur(' + obj.blur + ') brightness(' + obj.brightness + ') grayscale(' + obj.grayscale + ') sepia(' + obj.sepia + ') contrast(' + obj.contrast + ') opacity(' + obj.opacity + ') saturate(' + obj.saturate + ')';
1098            if( obj.$elt != null ) {
1099              for( var n = 0; n < obj.$elt.length; n++ ) {
1100                if( obj.$elt[n] != undefined ) {
1101                  obj.$elt[n].style.WebkitFilter = v;
1102                  obj.$elt[n].style.filter = v;
1103                }
1104              }
1105            }
1106          };
1107
1108          //--- CSS Filters - set a value in cache
1109          NGY2Item.prototype.CSSFilterSet = function ( eltClass, filter, value, forceRefresh ) {
1110            if( this.eltFilter[eltClass] == undefined ) {
1111              this.eltFilter[eltClass] = { blur: 0, brightness: '100%', grayscale: '0%', sepia: '0%', contrast: '100%', opacity: '100%', saturate: '100%' };
1112              this.eltFilter[eltClass].$elt = this.$getElt(eltClass);
1113            }
1114            this.eltFilter[eltClass][filter] = value;
1115            if( forceRefresh === true ) {
1116              this.eltTransform[eltClass].$elt = this.$getElt(eltClass, true);
1117            }
1118          };
1119
1120          //--- thumbnail hover animation
1121          NGY2Item.prototype.animate = function ( effect, delay, hoverIn ) {
1122            if( this.$getElt() == null  ) { return; }
1123
1124            var context = {};
1125            context.G = this.G;
1126            context.item = this;
1127            context.effect = effect;
1128            context.hoverIn = hoverIn;
1129            context.cssKind = '';
1130            if( hoverIn ) {
1131              // HOVER IN
1132              
1133              if( this.eltEffect[effect.element] == undefined ) {
1134                this.eltEffect[effect.element] = [];
1135              }
1136              if( this.eltEffect[effect.element][effect.type] == undefined ) {
1137                this.eltEffect[effect.element][effect.type] = { initialValue: 0, lastValue: 0 };
1138              }
1139              if( effect.firstKeyframe ) {
1140                // store initial and current value -> for use in the back animation
1141                this.eltEffect[effect.element][effect.type] = { initialValue: effect.from, lastValue: effect.from};
1142              }
1143              
1144              context.animeFrom = effect.from;
1145              context.animeTo = effect.to;
1146              context.animeDuration = parseInt(effect.duration);
1147              context.animeDelay = 30 + parseInt(effect.delay + delay);  // 30ms is a default delay to avoid conflict with other initializations
1148              context.animeEasing = effect.easing;
1149            }
1150            else {
1151              // HOVER OUT
1152              // if( effect.firstKeyframe ) {
1153                context.animeFrom = this.eltEffect[effect.element][effect.type].lastValue;
1154                context.animeTo = this.eltEffect[effect.element][effect.type].initialValue;
1155                // context.animeTo=effect.from;
1156              // }
1157              // else {
1158                // // context.animeFrom=effect.from;
1159                // context.animeFrom = this.eltEffect[effect.element][effect.type].lastValue;
1160                // context.animeTo = this.eltEffect[effect.element][effect.type].initialValue;
1161                // //context.animeTo=effect.to;
1162                
1163              // }
1164              
1165              context.animeDuration = parseInt(effect.durationBack);
1166              context.animeDelay = 30 + parseInt(effect.delayBack + delay);   // 30ms is a default delay to avoid conflict with other initializations
1167              context.animeEasing = effect.easingBack;
1168            }
1169            
1170
1171            // detect if animation on CSS transform
1172            var transform=['translateX', 'translateY', 'translateZ', 'scale', 'rotateX', 'rotateY', 'rotateZ'];
1173            for( var i = 0; i < transform.length; i++ ) {
1174              if( effect.type == transform[i] ) {
1175                context.cssKind = 'transform';
1176                break;
1177              }
1178            }
1179
1180            // detect if animation on CSS filter
1181            var filter=['blur', 'brightness', 'grayscale', 'sepia', 'contrast', 'opacity', 'saturate'];
1182            for( var i = 0; i < filter.length; i++ ) {
1183              if( effect.type == filter[i] ) {
1184                context.cssKind = 'filter';
1185                break;
1186              }
1187            }
1188            // handle some special cases
1189            if( hoverIn && effect.element == '.nGY2GThumbnail' && ( effect.type == 'scale' || effect.type == 'rotateX') ) {
1190              this.G.GOM.lastZIndex++;
1191              this.$getElt(effect.element).css('z-index', this.G.GOM.lastZIndex);
1192              // setElementOnTop(this.G.$E.base, this.$getElt(effect.element) );
1193            }
1194            
1195            // animation
1196            var tweenable = new NGTweenable();
1197            context.tweenable=tweenable;
1198            tweenable.tween({
1199              attachment:   context,
1200              from:         { 'v': context.animeFrom },
1201              to:           { 'v': context.animeTo },
1202              duration:     context.animeDuration,    //parseInt(effect.duration),
1203              delay:        context.animeDelay,       //parseInt(effect.delay),
1204              easing:       context.animeEasing,      //'easeOutQuart',
1205              
1206              step: function (state, att) {
1207                if( att.item.$getElt() == null ) {
1208                  // the thumbnail may be destroyed since the start of the animation
1209                  att.tweenable.stop(false);
1210                  // att.tweenable.dispose();
1211                  return;
1212                }
1213                if( att.hoverIn && !att.item.hovered ) {
1214                  // thumbnail no more hovered
1215                  att.tweenable.stop(false);
1216                  // att.tweenable.dispose();
1217                  return;
1218                }
1219
1220                if( att.G.VOM.viewerDisplayed ) {
1221                  att.tweenable.stop(false);
1222                  // att.tweenable.dispose();
1223                  return;
1224                }
1225                
1226                // test if in delay phase
1227                if( state.v == att.animeFrom ) { return; }
1228                
1229                switch( att.cssKind ) {
1230                  case 'transform':
1231                    // window.ng_draf( function() {
1232                      att.item.CSSTransformSet(att.effect.element, att.effect.type, state.v);
1233                      att.item.CSSTransformApply( att.effect.element );
1234                    // });
1235                    break;
1236                  case 'filter':
1237                    // window.ng_draf( function() {
1238                      att.item.CSSFilterSet(att.effect.element, att.effect.type, state.v);
1239                      att.item.CSSFilterApply( att.effect.element );
1240                    // });
1241                    break;
1242                  default:
1243                    var v=state.v;
1244                    if( state.v.substring(0,4) == 'rgb(' || state.v.substring(0,5) == 'rgba(' ) {
1245                      // to remove values after the dot (not supported by RGB/RGBA)
1246                      // v=ngtinycolor(state.v).toRgbString();
1247                      v = ShadeBlendConvert(0, v);
1248                    }
1249                    // window.ng_draf( function() {
1250                      att.item.$getElt( att.effect.element ).css( att.effect.type, v );
1251                    // });
1252                    break;
1253                }
1254                if( hoverIn ) {
1255                  // store value for back animation
1256                  att.item.eltEffect[att.effect.element][att.effect.type].lastValue = state.v;
1257                }
1258              },
1259              
1260              finish: function (state, att) {
1261                if( hoverIn ) {
1262                  // store value for back animation
1263                  att.item.eltEffect[att.effect.element][att.effect.type].lastValue = state.v;
1264                }
1265
1266                if( att.item.$getElt() == null ) {
1267                  // the thumbnail may be destroyed since the start of the animation
1268                  return;
1269                }
1270                if( att.hoverIn && !att.item.hovered ) {
1271                  // thumbnail no more hovered
1272                  return;
1273                }
1274
1275                if( att.G.VOM.viewerDisplayed ) {
1276                  return;
1277                }
1278
1279                // window.ng_draf( function() {
1280                  switch( att.cssKind ) {
1281                    case 'transform':
1282                      att.item.CSSTransformSet(att.effect.element, att.effect.type, att.animeTo);
1283                      att.item.CSSTransformApply(att.effect.element);
1284                      break;
1285                    case 'filter':
1286                      att.item.CSSFilterSet(att.effect.element, att.effect.type, att.animeTo);
1287                      att.item.CSSFilterApply(att.effect.element);
1288                      break;
1289                    default:
1290                      att.item.$getElt(att.effect.element).css(att.effect.type, att.animeTo);
1291                      break;
1292                  }
1293                // });
1294              }
1295            });
1296          };
1297
1298          return NGY2Item;
1299        })();
1300          
1301      }
1302
1303      _this.options = jQuery.extend(true, {}, jQuery.nanogallery2.defaultOptions, options);
1304      // Initialization code
1305      _this.nG2 = null;
1306      _this.nG2 = new nanoGALLERY2();
1307      _this.nG2.initiateGallery2(_this.e, _this.options );
1308
1309    };
1310      
1311    // PUBLIC EXPOSED METHODS
1312    _this.test = function() {
1313      //alert('test');
1314      // console.dir(_this.nG.G.I.length);
1315      // console.dir(_this.nG);
1316      // debugger;
1317      //privateTest();
1318    }
1319
1320    
1321    // Run initializer
1322    _this.init();
1323  };
1324 
1325  jQuery.nanogallery2.defaultOptions = {
1326    kind :                        '',
1327    userID :                      '',
1328    photoset :                    '',
1329    album:                        '',
1330    blockList :                   'scrapbook|profil|auto backup',
1331    tagBlockList:                 '',
1332    allowList :                   '',
1333    albumList :                   '',
1334    albumList2 :                  null,
1335    RTL :                         false,
1336    flickrSkipOriginal :          true,
1337    flickrAPIKey:                 '',
1338    breadcrumbAutoHideTopLevel :  true,
1339    displayBreadcrumb :           true,
1340    breadcrumbOnlyCurrentLevel :  true,
1341    breadcrumbHideIcons :         true,
1342    theme :                       'nGY2',
1343    galleryTheme :                'dark',
1344    viewerTheme :                 'dark',
1345    items :                       null,
1346    itemsBaseURL :                '',
1347    thumbnailSelectable :         false,
1348    dataProvider:                 '',
1349    allowHTMLinData:              false,
1350    locationHash :                true,
1351    slideshowDelay :              3000,
1352    slideshowAutoStart :          false,
1353
1354    debugMode:                    false,
1355
1356    titleTranslationMap:          null,
1357    galleryDisplayMoreStep :      2,
1358    galleryDisplayMode :          'fullContent',
1359    galleryL1DisplayMode :        null,
1360    galleryPaginationMode :       'rectangles',   // 'dots', 'rectangles', 'numbers'
1361    galleryPaginationTopButtons : true,
1362    galleryMaxRows :              2,
1363    galleryL1MaxRows :            null,
1364    galleryLastRowFull:           false,
1365    galleryL1LastRowFull:         null,
1366    galleryLayoutEngine :         'default',
1367    paginationSwipe:              true,
1368    paginationVisiblePages :      10,
1369    galleryFilterTags :           false,    // possible values: false, true, 'title', 'description'
1370    galleryL1FilterTags :         null,     // possible values: false, true, 'title', 'description'
1371    galleryFilterTagsMode :       'single',
1372    galleryL1FilterTagsMode :     null,
1373    galleryMaxItems :             0,        // maximum number of items per album  --> only flickr, google2, nano_photos_provider2
1374    galleryL1MaxItems :           null,     // maximum number of items per gallery page --> only flickr, google2, nano_photos_provider2
1375    gallerySorting :              '',
1376    galleryL1Sorting :            null,
1377    galleryDisplayTransition :    'none',
1378    galleryL1DisplayTransition :  null,
1379    galleryDisplayTransitionDuration :    1000,
1380    galleryL1DisplayTransitionDuration :  null,
1381    galleryResizeAnimation :      false,
1382    galleryRenderDelay :          10,
1383
1384    thumbnailCrop :               true,
1385    thumbnailL1Crop :             null,
1386    thumbnailCropScaleFactor :    1.5,
1387    thumbnailLevelUp :            false,
1388    thumbnailAlignment :          'fillWidth',
1389    thumbnailWidth :              300,
1390    thumbnailL1Width :            null,
1391    thumbnailHeight :             200,
1392    thumbnailL1Height :           null,
1393    thumbnailBaseGridHeight :     0,
1394    thumbnailL1BaseGridHeight :   null,
1395    thumbnailGutterWidth :        2,
1396    thumbnailL1GutterWidth :      null,
1397    thumbnailGutterHeight :       2,
1398    thumbnailL1GutterHeight :     null,
1399    thumbnailBorderVertical :     2,
1400    thumbnailL1BorderVertical :   null,
1401    thumbnailBorderHorizontal :   2,
1402    thumbnailL1BorderHorizontal : null,
1403    thumbnailFeaturedKeyword :    '*featured',
1404    thumbnailAlbumDisplayImage :  false,
1405    thumbnailHoverEffect2 :       'toolsAppear',
1406    thumbnailBuildInit2 :         '',
1407    thumbnailStacks :             0,
1408    thumbnailL1Stacks :           null,
1409    thumbnailStacksTranslateX :   0,
1410    thumbnailL1StacksTranslateX : null,
1411    thumbnailStacksTranslateY :   0,
1412    thumbnailL1StacksTranslateY : null,
1413    thumbnailStacksTranslateZ :   0,
1414    thumbnailL1StacksTranslateZ : null,
1415    thumbnailStacksRotateX :      0,
1416    thumbnailL1StacksRotateX :    null,
1417    thumbnailStacksRotateY :      0,
1418    thumbnailL1StacksRotateY :    null,
1419    thumbnailStacksRotateZ :      0,
1420    thumbnailL1StacksRotateZ :    null,
1421    thumbnailStacksScale :        0,
1422    thumbnailL1StacksScale :      null,
1423    thumbnailDisplayOutsideScreen: true,
1424    thumbnailWaitImageLoaded:     true,
1425    thumbnailSliderDelay:         2000,
1426    galleryBuildInit2 :           '',
1427    portable :                    false,
1428    eventsDebounceDelay:          10,
1429    
1430    touchAnimation :              false,
1431    touchAnimationL1 :            undefined,
1432    touchAutoOpenDelay :          0,
1433
1434    thumbnailLabel : {
1435      position :                  'overImage',
1436      align:                      'center',
1437      valign:                     'bottom',
1438      display :                   true,
1439      displayDescription :        false,
1440      titleMaxLength :            0,
1441      titleMultiLine :            false,
1442      descriptionMaxLength :      0,
1443      descriptionMultiLine :      false,
1444      hideIcons :                 true,
1445      title :                     ''
1446    },
1447
1448    thumbnailToolbarImage :       { topLeft: 'select', topRight : 'featured' },
1449    thumbnailToolbarAlbum :       { topLeft: 'select', topRight : 'counter'  },
1450    thumbnailDisplayOrder :       '',
1451    thumbnailL1DisplayOrder :     null,
1452    thumbnailDisplayInterval :    15,
1453    thumbnailL1DisplayInterval :  null,
1454    thumbnailDisplayTransition :  'fadeIn',
1455    thumbnailL1DisplayTransition : null,
1456    thumbnailDisplayTransitionEasing :  'easeOutQuart',
1457    thumbnailL1DisplayTransitionEasing : null,
1458    thumbnailDisplayTransitionDuration:   240,
1459    thumbnailL1DisplayTransitionDuration: null,
1460    thumbnailOpenInLightox :      true,
1461    thumbnailOpenOriginal :       false,
1462    
1463    lightboxStandalone:						false,
1464		viewer :                      'internal',
1465    viewerFullscreen:             false,
1466    imageTransition :             'swipe2',
1467    viewerTransitionMediaKind :   'img',
1468    viewerZoom :                  true,
1469    viewerImageDisplay :          '',
1470    openOnStart :                 '',
1471    viewerHideToolsDelay :        4000,
1472    viewerToolbar : {
1473      display :                   false,
1474      position :                  'bottom',
1475      fullWidth :                 false,
1476      align :                     'center',
1477      autoMinimize :              0,
1478      standard :                  'minimizeButton,label',
1479      minimized :                 'minimizeButton,label,infoButton,shareButton,fullscreenButton'
1480    },
1481    viewerTools : {
1482      topLeft :                   'pageCounter,playPauseButton',
1483      topRight :                  'rotateLeft,rotateRight,fullscreenButton,closeButton' 
1484    },
1485		viewerGallery:								'bottomOverMedia',
1486		viewerGalleryTWidth:					40,
1487		viewerGalleryTHeight:  				40,
1488    
1489    breakpointSizeSM :            480,
1490    breakpointSizeME :            992,
1491    breakpointSizeLA :            1200,
1492    breakpointSizeXL :            1800,
1493    
1494    fnThumbnailInit :             null,
1495    fnThumbnailHoverInit :        null,
1496    fnThumbnailHover :            null,
1497    fnThumbnailHoverOut :         null,
1498    fnThumbnailDisplayEffect :    null,
1499    fnViewerInfo :                null,
1500    fnImgToolbarCustInit :        null,
1501    fnImgToolbarCustDisplay :     null,
1502    fnImgToolbarCustClick :       null,
1503    fnProcessData :               null,
1504    fnThumbnailSelection :        null,
1505    fnGalleryRenderStart :        null,
1506    fnGalleryRenderEnd :          null,
1507    fnGalleryObjectModelBuilt :   null,
1508    fnGalleryLayoutApplied :      null,
1509    fnThumbnailClicked :          null,
1510    fnShoppingCartUpdated :       null,
1511    fnThumbnailToolCustAction :   null,
1512    fnThumbnailOpen :             null,
1513    fnImgDisplayed :              null,
1514    fnPopupMediaInfo :            null,
1515
1516    i18n : {
1517      'breadcrumbHome' : 'Galleries', 'breadcrumbHome_FR' : 'Galeries',
1518      'thumbnailImageTitle' : '', 'thumbnailAlbumTitle' : '',
1519      'thumbnailImageDescription' : '', 'thumbnailAlbumDescription' : '',
1520      'infoBoxPhoto' : 'Photo', 'infoBoxDate' : 'Date', 'infoBoxAlbum' : 'Album', 'infoBoxDimensions' : 'Dimensions', 'infoBoxFilename' : 'Filename', 'infoBoxFileSize' : 'File size', 'infoBoxCamera' : 'Camera', 'infoBoxFocalLength' : 'Focal length', 'infoBoxExposure' : 'Exposure', 'infoBoxFNumber' : 'F Number', 'infoBoxISO' : 'ISO', 'infoBoxMake' : 'Make', 'infoBoxFlash' : 'Flash', 'infoBoxViews' : 'Views', 'infoBoxComments' : 'Comments'
1521    },
1522    icons : {
1523      // example for font awesome: <i style="color:#eee;" class="fa fa-search-plus"></i>
1524      thumbnailAlbum:               '<i class="nGY2Icon-folder-empty"></i>',
1525      thumbnailImage:               '<i class="nGY2Icon-picture"></i>',
1526      breadcrumbAlbum:              '<i class="nGY2Icon-folder-empty"></i>',
1527      breadcrumbHome:               '<i class="nGY2Icon-home"></i>',
1528      breadcrumbSeparator:          '<i class="nGY2Icon-left-open"></i>',
1529      breadcrumbSeparatorRtl:       '<i class="nGY2Icon-right-open"></i>',
1530      navigationFilterSelected:     '<i style="color:#fff;" class="nGY2Icon-ok"></i>',
1531      navigationFilterUnselected:   '<i style="color:#ddd;opacity:0.3;" class="nGY2Icon-circle-empty"></i>',
1532      navigationFilterSelectedAll:  '<i class="nGY2Icon-ccw"></i>',
1533      navigationPaginationPrevious: '<i class="nGY2Icon-ngy2_chevron-left"></i>',
1534      navigationPaginationNext:     '<i class="nGY2Icon-ngy2_chevron-right"></i>',
1535      thumbnailSelected:            '<i style="color:#bff;" class="nGY2Icon-ok-circled"></i>',
1536      thumbnailUnselected:          '<i style="color:#bff;" class="nGY2Icon-circle-empty"></i>',
1537      thumbnailFeatured:            '<i style="color:#dd5;" class="nGY2Icon-star"></i>',
1538      thumbnailCounter:             '<i class="nGY2Icon-picture"></i>',
1539      thumbnailShare:               '<i class="nGY2Icon-ngy2_share2"></i>',
1540      thumbnailDownload:            '<i class="nGY2Icon-ngy2_download2"></i>',
1541      thumbnailInfo:                '<i class="nGY2Icon-ngy2_info2"></i>',
1542      thumbnailShoppingcart:        '<i class="nGY2Icon-basket"></i>',
vendor: 203,093 bytes, lines 1543-6930
1543      thumbnailDisplay:             '<i class="nGY2Icon-resize-full"></i>',
1544      thumbnailCustomTool1:         'T1',
1545      thumbnailCustomTool2:         'T2',
1546      thumbnailCustomTool3:         'T3',
1547      thumbnailCustomTool4:         'T4',
1548      thumbnailCustomTool5:         'T5',
1549      thumbnailCustomTool6:         'T6',
1550      thumbnailCustomTool7:         'T7',
1551      thumbnailCustomTool8:         'T8',
1552      thumbnailCustomTool9:         'T9',
1553      thumbnailCustomTool10:        'T10',
1554      thumbnailAlbumUp:             '<i style="font-size: 3em;" class="nGY2Icon-ngy2_chevron_up2"></i>',
1555      paginationNext:               '<i class="nGY2Icon-right-open"></i>',
1556      paginationPrevious:           '<i class="nGY2Icon-left-open"></i>',
1557      galleryMoreButton:            '<i class="nGY2Icon-picture"></i> &nbsp; <i class="nGY2Icon-right-open"></i>',
1558      buttonClose:                  '<i class="nGY2Icon-ngy2_close2"></i>',
1559      viewerPrevious:               '<i class="nGY2Icon-ngy2_chevron-left"></i>',
1560      viewerNext:                   '<i class="nGY2Icon-ngy2_chevron-right"></i>',
1561      viewerImgPrevious:            '<i class="nGY2Icon-ngy2_chevron_left3"></i>',
1562      viewerImgNext:                '<i class="nGY2Icon-ngy2_chevron_right3"></i>',
1563      viewerDownload:               '<i class="nGY2Icon-ngy2_download2"></i>',
1564      viewerToolbarMin:             '<i class="nGY2Icon-ellipsis-vert"></i>',
1565      viewerToolbarStd:             '<i class="nGY2Icon-menu"></i>',
1566      viewerPlay:                   '<i class="nGY2Icon-play"></i>',
1567      viewerPause:                  '<i class="nGY2Icon-pause"></i>',
1568      viewerFullscreenOn:           '<i class="nGY2Icon-resize-full"></i>',
1569      viewerFullscreenOff:          '<i class="nGY2Icon-resize-small"></i>',
1570      viewerZoomIn:                 '<i class="nGY2Icon-ngy2_zoom_in2"></i>',
1571      viewerZoomOut:                '<i class="nGY2Icon-ngy2_zoom_out2"></i>',
1572      viewerLinkOriginal:           '<i class="nGY2Icon-ngy2_external2"></i>',
1573      viewerInfo:                   '<i class="nGY2Icon-ngy2_info2"></i>',
1574      viewerShare:                  '<i class="nGY2Icon-ngy2_share2"></i>',
1575      viewerRotateLeft:             '<i class="nGY2Icon-ccw"></i>',
1576      viewerRotateRight:            '<i class="nGY2Icon-cw"></i>',
1577      viewerShoppingcart:           '<i class="nGY2Icon-basket"></i>',
1578      user:                         '<i class="nGY2Icon-user"></i>',
1579      location:                     '<i class="nGY2Icon-location"></i>',
1580      picture:                     '<i class="nGY2Icon-picture"></i>',
1581      config:                       '<i class="nGY2Icon-wrench"></i>',
1582      shareFacebook:                '<i style="color:#3b5998;" class="nGY2Icon-facebook-squared"></i>',
1583      shareTwitter:                 '<i style="color:#00aced;" class="nGY2Icon-twitter-squared"></i>',
1584      // shareGooglePlus:              '<i style="color:#dd4b39;" class="nGY2Icon-gplus-squared"></i>',
1585      shareTumblr:                  '<i style="color:#32506d;" class="nGY2Icon-tumblr-squared"></i>',
1586      sharePinterest:               '<i style="color:#cb2027;" class="nGY2Icon-pinterest-squared"></i>',
1587      shareVK:                      '<i style="color:#3b5998;" class="nGY2Icon-vkontakte"></i>',
1588      shareMail:                    '<i style="color:#555;" class="nGY2Icon-mail-alt"></i>',
1589      viewerCustomTool1:            'T1',
1590      viewerCustomTool2:            'T2',
1591      viewerCustomTool3:            'T3',
1592      viewerCustomTool4:            'T4',
1593      viewerCustomTool5:            'T5',
1594      viewerCustomTool6:            'T6',
1595      viewerCustomTool7:            'T7',
1596      viewerCustomTool8:            'T8',
1597      viewerCustomTool9:            'T9',
1598      viewerCustomTool10:           'T10'
1599    }
1600  };
1601
1602  jQuery.fn.nanogallery2 = function (args, option, value) {
1603
1604	if( typeof jQuery(this).data('nanogallery2data') === 'undefined'){
1605      if( args == 'destroy' ) {
1606        // command to destroy but no instance yet --> exit
1607        return;
1608      }
1609      
1610      return this.each( function(){
1611        (new jQuery.nanogallery2(this, args));
1612      });
1613    }
1614    else {
1615			// no options -->
1616      // This function breaks the chain, but provides some API methods
1617      var nG2 = $(this).data('nanogallery2data').nG2;
1618
1619			// Lightbox standalone
1620      // (Another click on an already opened media)
1621			if( args !== undefined && args.lightboxStandalone === true ) {
1622        // items exist already (G.I is populated) -> just open the lightbox again
1623        nG2.LightboxReOpen();
1624				return;
1625			}
1626			
1627      switch(args){
1628        case 'displayItem':
1629          nG2.DisplayItem(option);
1630          break;
1631          
1632        case 'search':
1633          return( nG2.Search(option));
1634          break;
1635          
1636        case 'search2':
1637          return nG2.Search2(option, value);
1638          break;
1639          
1640        case 'search2Execute':
1641          return nG2.Search2Execute();
1642          break;
1643          
1644        case 'refresh':
1645          nG2.Refresh();
1646          break;
1647
1648        case 'resize':
1649          nG2.Resize();
1650          break;
1651          
1652        case 'instance':
1653          return nG2;
1654          break;
1655          
1656        case 'data':
1657          nG2.data= {
1658            items: nG2.I,
1659            gallery: nG2.GOM,
1660            lightbox: nG2.VOM,
1661						shoppingcart: nG2.shoppingCart
1662          };
1663          return nG2.data;
1664          break;
1665          
1666        case 'reload':
1667          nG2.ReloadAlbum();
1668          return $(this);
1669          break;
1670          
1671        case 'itemsSelectedGet':
1672          return nG2.ItemsSelectedGet();
1673          break;
1674          
1675        case 'itemsSetSelectedValue':
1676          nG2.ItemsSetSelectedValue(option, value);
1677          break;
1678          
1679        case 'option':
1680          if(typeof value === 'undefined'){
1681            return nG2.Get(option);
1682          }else{
1683            nG2.Set(option,value);
1684            if( option == 'demoViewportWidth' ) {
1685              // force resize event -> for demo purposes
1686              $(window).trigger('resize');
1687            }
1688          }
1689          break;
1690          
1691        case 'destroy':
1692          nG2.Destroy();
1693          $(this).removeData('nanogallery2data');
1694          break;
1695          
1696        case 'shoppingCartGet':
1697          // returns the content of the shoppingcart
1698          return nG2.shoppingCart;
1699          break;
1700          
1701        case 'shoppingCartUpdate':
1702          // parameters :
1703          //  - option = item's ID
1704          //  - value = new quantity
1705			
1706          if( typeof value === 'undefined' || typeof option === 'undefined' ){
1707            return false;
1708          }
1709          
1710          var item_ID = option;
1711          var new_qty = value;
1712
1713          for( var i=0; i < nG2.shoppingCart.length; i++) {
1714            if( nG2.shoppingCart[i].ID == item_ID ) {
1715              
1716              // updates counter
1717              nG2.shoppingCart[i].qty = new_qty;
1718              
1719              let item = nG2.I[nG2.shoppingCart[i].idx];
1720
1721              // updates thumbnail
1722              nG2.ThumbnailToolbarOneCartUpdate( item );
1723              
1724              if( new_qty == 0 ) {
1725                // removes item from shoppingcart
1726                nG2.shoppingCart.splice(i, 1);
1727              }
1728              
1729              var fu = nG2.O.fnShoppingCartUpdated;
1730              if( fu !== null ) {
1731                typeof fu == 'function' ? fu(nG2.shoppingCart, item, 'api') : window[fu](nG2.shoppingCart, item, 'api');
1732              }
1733
1734              break;
1735            }
1736          }
1737         
1738          return nG2.shoppingCart;
1739          break;
1740          
1741        case 'shoppingCartRemove':
1742          // parameters :
1743          //  - option = item's ID
1744          if( typeof option === 'undefined' ){
1745            return false;
1746          }
1747          var ID = option;
1748          for( var i=0; i < nG2.shoppingCart.length; i++) {
1749            if( nG2.shoppingCart[i].ID == ID ) {
1750              
1751              var item = nG2.I[nG2.shoppingCart[i].idx];
1752
1753              // updates thumbnail
1754              nG2.shoppingCart[i].qty = 0;
1755              nG2.ThumbnailToolbarOneCartUpdate( item );
1756              
1757              // removes item from shoppingcart
1758              nG2.shoppingCart.splice(i, 1);
1759              
1760              
1761              var fu = nG2.O.fnShoppingCartUpdated;
1762              if( fu !== null ) {
1763                typeof fu == 'function' ? fu(nG2.shoppingCart, item, 'api') : window[fu](nG2.shoppingCart, item, 'api');
1764              }
1765
1766              break;
1767            }
1768          }
1769          
1770          return nG2.shoppingCart;
1771          break;
1772         
1773        case 'closeViewer':
1774          nG2.CloseViewer();
1775          break;
1776        case 'minimizeToolbar':
1777          nG2.MinimizeToolbar();
1778          break;
1779        case 'maximizeToolbar':
1780          nG2.MaximizeToolbar();
1781          break;
1782        case 'paginationPreviousPage':
1783          nG2.PaginationPreviousPage();
1784          break;
1785        case 'paginationNextPage':
1786          nG2.paginationNextPage();
1787          break;
1788        case 'paginationGotoPage':
1789          nG2.PaginationGotoPage( option );
1790          break;
1791        case 'paginationCountPages':
1792          nG2.PaginationCountPages();
1793          break;
1794         
1795      }
1796      return $(this);
1797
1798    }
1799  };
1800  
1801
1802  // ###############################
1803  // ##### nanogallery2 script #####
1804  // ###############################
1805
1806  /** @function nanoGALLERY2 */
1807  function nanoGALLERY2() {
1808    "use strict";
1809
1810    /**
1811    * Force reload the current album, if provided by Json
1812    */
1813    this.LightboxReOpen = function(){
1814        LightboxStandaloneDisplay();
1815    }
1816
1817    /**
1818    * Force reload the current album, if provided by Json
1819    */
1820    this.ReloadAlbum = function(){
1821      if( G.O.kind === '' ) {
1822        throw 'Not supported for this content source:' + G.O.kind;
1823      }
1824
1825      var albumIdx = G.GOM.albumIdx;
1826      if( albumIdx == -1 ) {
1827        throw ('Current album not found.');
1828      }
1829      
1830      var albumID = G.I[albumIdx].GetID();
1831
1832      // unselect everything & remove link to album (=logical delete)
1833      var l = G.I.length;
1834      for( var i = 0; i < l ; i++ ) {
1835        var item = G.I[i];
1836        if( item.albumID == albumID ) {
1837          item.selected = false;
1838        }
1839      }
1840      
1841      G.I[albumIdx].contentIsLoaded = false;
1842      
1843      DisplayAlbum('-1', albumID);
1844    };
1845    
1846    /**
1847     * Set one or several items selected/unselected
1848     * @param {array} items
1849     */
1850    this.ItemsSetSelectedValue = function(items, value){
1851      var l = items.length;
1852      for( var j = 0; j < l ; j++) {
1853        ThumbnailSelectionSet(items[j], value);
1854      }
1855    };
1856    
1857    /**
1858     * Returns an array of selected items
1859     * @returns {Array}
1860     */
1861    this.ItemsSelectedGet = function(){
1862      var selectedItems = [];
1863      var l = G.I.length;
1864      for( var i = 0; i < l ; i++ ) {
1865        if( G.I[i].selected == true ) {
1866          selectedItems.push(G.I[i]);
1867        }
1868      }
1869      return selectedItems;
1870    };    
1871    
1872    /**
1873     * Returns the value of an option
1874     * @param {string} option
1875     * @returns {nanoGALLERY.G.O}
1876     */
1877    this.Get = function(option){
1878        return G.O[option];
1879    };
1880
1881    /**
1882     * Set a new value for a defined option
1883     * @param {string} option
1884     */
1885    this.Set = function(option, value){
1886        G.O[option] = value;
1887        switch( option ) {
1888          case 'thumbnailSelectable':
1889            ThumbnailSelectionClear();
1890            // refresh the displayed gallery
1891            GalleryRender( G.GOM.albumIdx );
1892            break;
1893        }
1894    };
1895    
1896    /**
1897     * Refresh the current gallery
1898     */
1899    this.Refresh = function() {
1900      // Refresh the displayed gallery
1901      GalleryRender( G.GOM.albumIdx );
1902    };
1903    /**
1904     * Resize the current gallery
1905     */
1906    this.Resize = function() {
1907      // resize the displayed gallery
1908      GalleryResize();
1909    };
1910
1911    /**
1912     * display one item (image or gallery)
1913     *   itemID syntax:
1914     *    - albumID --> display one album
1915     *    - albumID/imageID --> display one image
1916    */
1917    this.DisplayItem = function( itemID ) {
1918      var IDs=parseIDs( itemID );
1919      if( IDs.imageID != '0' ) {
1920        DisplayPhoto( IDs.imageID, IDs.albumID );
1921      }
1922      else {
1923        DisplayAlbum( '-1', IDs.albumID );
1924      }
1925    };
1926    
1927    this.ThumbnailToolbarOneCartUpdate = function ( item ) {
1928      ThumbnailBuildToolbarOneCartUpdate( item );
1929    }
1930    
1931      
1932      
1933    var CountItemsToDisplay = function( gIdx ) {
1934      if( G.I[gIdx] == undefined ) { return 0; }
1935      var albumID = G.I[gIdx].GetID();
1936      var l = G.I.length;
1937      var cnt = 0;
1938      for( var idx = 0; idx < l; idx++ ) {
1939        var item = G.I[idx];
1940        if( item.isToDisplay(albumID) ) {
1941          cnt++;
1942        }
1943      }
1944      return cnt;
1945    } 
1946    /**
1947     * Search in the displayed gallery (in thumbnails title)
1948     */
1949    this.Search = function( search ) {
1950      G.GOM.albumSearch = search.toUpperCase();
1951      var gIdx = G.GOM.albumIdx;
1952      GalleryRender( G.GOM.albumIdx );
1953      return CountItemsToDisplay( gIdx );
1954    };
1955
1956    /**
1957     * Search2 in title and tags - set search values
1958     */
1959    this.Search2 = function( searchTitle, searchTags ) {
1960      if( searchTitle != undefined && searchTitle != null  ) {
1961        G.GOM.albumSearch = searchTitle.toUpperCase().trim();
1962      }
1963      else {
1964        G.GOM.albumSearch = '';
1965      }
1966      
1967      if( searchTags != null && searchTags != undefined ) {
1968        G.GOM.albumSearchTags = searchTags.toUpperCase().trim();
1969      }
1970      else {
1971        G.GOM.albumSearchTags = '';
1972      }
1973      return CountItemsToDisplay( G.GOM.albumIdx );
1974    };
1975    
1976    /**
1977     * Search2 - execute the search on title and tags
1978     */
1979    this.Search2Execute = function() {
1980      var gIdx = G.GOM.albumIdx;
1981      GalleryRender( G.GOM.albumIdx );
1982      return CountItemsToDisplay( gIdx );
1983    };
1984    
1985    
1986    /**
1987     * Destroy the current gallery
1988     */
1989    this.Destroy = function(){
1990
1991      if( G.GOM.hammertime != null ) {
1992        G.GOM.hammertime.destroy();
1993        G.GOM.hammertime = null;
1994      }
1995
1996      if( G.VOM.hammertime != null ) {
1997        G.VOM.hammertime.destroy();
1998        G.VOM.hammertime = null;
1999      }
2000      
2001      // color scheme
2002      $('#ngycs_' + G.baseEltID).remove()
2003      
2004      G.GOM.items = [];
2005      NGY2Item.New( G, G.i18nTranslations.breadcrumbHome, '', '0', '-1', 'album' );
2006      G.GOM.navigationBar.$newContent = null;
2007      G.$E.base.empty();
2008      G.$E.base.removeData();
2009      if( G.O.locationHash ) {
2010        jQuery(window).off('hashchange.nanogallery2.' + G.baseEltID);
2011      }
2012
2013      jQuery(window).off('resize.nanogallery2.' + G.baseEltID);
2014      jQuery(window).off('orientationChange.nanogallery2.' + G.baseEltID);
2015      jQuery(window).off('scroll.nanogallery2.' + G.baseEltID);
2016			if( G.$E.scrollableParent !== null ) {
2017				G.$E.scrollableParent.off('scroll.nanogallery2.' + G.baseEltID);
2018			}
2019      G.GOM.firstDisplay = true;
2020    };
2021    
2022    /**
2023     * CloseViewer - close the media viewer
2024     */
2025    this.CloseViewer = function() {
2026      LightboxClose(null);
2027      return false;
2028    };
2029    
2030    /**
2031     * MinimizeToolbar - display the minimized lightbox main toolbar
2032     */
2033    this.MinimizeToolbar = function() {
2034      ViewerToolbarForVisibilityMin();
2035      return false;
2036    };
2037    
2038    /**
2039     * MaximizeToolbar - display the maximized/standard lightbox main toolbar
2040     */
2041    this.MaximizeToolbar = function() {
2042      ViewerToolbarForVisibilityStd();
2043      return false;
2044    };
2045    
2046    /**
2047     * PaginationPreviousPage - gallery paginate to previous page
2048     */
2049    this.PaginationPreviousPage = function() {
2050      paginationPreviousPage();
2051      return false;
2052    };
2053    
2054    
2055    /**
2056     * PaginationNextPage - gallery paginate to next page
2057     */
2058    this.PaginationNextPage = function() {
2059      paginationNextPage();
2060      return false;
2061    };
2062    
2063    
2064    /**
2065     * PaginationGotoPage - gallery paginate to specific page
2066     */
2067    this.PaginationGotoPage = function( page ) {
2068      // var aIdx = G.$E.conPagin.data('galleryIdx');
2069      if( page > 1 ) { page--; }
2070      G.GOM.pagination.currentPage = page;
2071
2072      // scroll to top of gallery if not displayed
2073      G.GOM.ScrollToTop();
2074
2075      GalleryDisplayPart1();
2076      GalleryDisplayPart2( true );
2077      return false;
2078    };
2079
2080    /**
2081     * PaginationCountPages - gallery pagination - returns the number of pages
2082     */
2083    this.PaginationCountPages = function() {
2084      if( G.GOM.items.length == 0 ) { return 0; }   // no thumbnail to display
2085
2086      var nbPages = Math.ceil((G.GOM.items[G.GOM.items.length - 1].row + 1) / G.galleryMaxRows.Get());
2087      return nbPages;
2088    };
2089    
2090    /**
2091     * PaginationCountPages - gallery pagination - returns the number of pages
2092     */
2093
2094    
2095    
2096    // throttle()
2097    // author: underscore.js - http://underscorejs.org/docs/underscore.html
2098    // Returns a function, that, when invoked, will only be triggered at most once during a given window of time.
2099    // Normally, the throttled function will run as much as it can, without ever going more than once per wait duration;
2100    // but if youÂ’d like to disable the execution on the leading edge, pass {leading: false}.
2101    // To disable execution on the trailing edge, ditto.
2102    var throttle = function(func, wait, options) {
2103      var context, args, result;
2104      var timeout = null;
2105      var previous = 0;
2106      if (!options) options = {};
2107      var later = function() {
2108        previous = options.leading === false ? 0 : new Date().getTime();
2109        timeout = null;
2110        result = func.apply(context, args);
2111        if (!timeout) context = args = null;
2112      };
2113      return function() {
2114        var now = new Date().getTime();
2115        if (!previous && options.leading === false) previous = now;
2116        var remaining = wait - (now - previous);
2117        context = this;
2118        args = arguments;
2119        if (remaining <= 0 || remaining > wait) {
2120          if (timeout) {
2121            clearTimeout(timeout);
2122            timeout = null;
2123          }
2124          previous = now;
2125          result = func.apply(context, args);
2126          if (!timeout) context = args = null;
2127        } else if (!timeout && options.trailing !== false) {
2128          timeout = setTimeout(later, remaining);
2129        }
2130        return result;
2131      };
2132    };
2133    
2134    
2135    // DEBOUNCE
2136    // author: John Hann - http://unscriptable.com/2009/03/20/debouncing-javascript-methods/
2137    // execAsap - false means executing at the end of the detection period
2138    var debounce = function (func, threshold, execAsap) {
2139      var timeout;
2140      return function debounced () {
2141          var obj = this, args = arguments;
2142          function delayed () {
2143              if (!execAsap)
2144                  func.apply(obj, args);
2145              timeout = null; 
2146          };
2147   
2148          if (timeout)
2149              clearTimeout(timeout);
2150              // clearRequestTimeout(timeout);
2151          else if (execAsap)
2152              func.apply(obj, args);
2153          timeout = setTimeout(delayed, threshold || 100); 
2154          // timeout = requestTimeout(delayed, threshold || 100); 
2155      };
2156    }
2157    
2158
2159    // Double requestAnimationFrame
2160    window.ng_draf = function (cb) {
2161      return requestAnimationFrame(function() {
2162        window.requestAnimationFrame(cb)
2163      })
2164    }    
2165    
2166    // REQUESTTIMEOUT - replace SETTIMEOUT - https://gist.github.com/joelambert/1002116
2167    /**
2168     * Behaves the same as setTimeout except uses requestAnimationFrame() where possible for better performance
2169     * @param {function} fn The callback function
2170     * @param {int} delay The delay in milliseconds
2171     */
2172
2173    window.requestTimeout = function(fn, delay) {
2174      if( !window.requestAnimationFrame      	&& 
2175        !window.webkitRequestAnimationFrame && 
2176        !(window.mozRequestAnimationFrame && window.mozCancelRequestAnimationFrame) && // Firefox 5 ships without cancel support
2177        !window.oRequestAnimationFrame      && 
2178        !window.msRequestAnimationFrame)
2179          return window.setTimeout(fn, delay);
2180          
2181      var start = new Date().getTime(),
2182        handle = new Object();
2183        
2184      function loop(){
2185        var current = new Date().getTime(),
2186          delta = current - start;
2187          // delta = delay;
2188  
2189        delta >= delay ? fn.call() : handle.value = requestAnimFrame(loop);
2190      };
2191      
2192      handle.value = requestAnimFrame(loop);
2193      return handle;
2194    };
2195
2196    
2197    // requestAnimationFrame() shim by Paul Irish
2198    // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
2199    window.requestAnimFrame = (function() {
2200      return  window.requestAnimationFrame       || 
2201          window.webkitRequestAnimationFrame || 
2202          window.mozRequestAnimationFrame    || 
2203          window.oRequestAnimationFrame      || 
2204          window.msRequestAnimationFrame     || 
2205          function(/* function */ callback, /* DOMElement */ element){
2206            window.setTimeout(callback, 1000 / 60);
2207          };
2208    })();    
2209    
2210    
2211    // CLEARREQUESTTIMEOUT - to replace CLEARTIMEOUT - https://gist.github.com/joelambert/1002116
2212    /**
2213     * Behaves the same as clearTimeout except uses cancelRequestAnimationFrame() where possible for better performance
2214     * @param {int|object} fn The callback function
2215     */
2216    window.clearRequestTimeout = function(handle) {
2217        window.cancelAnimationFrame ? window.cancelAnimationFrame(handle.value) :
2218        window.webkitCancelAnimationFrame ? window.webkitCancelAnimationFrame(handle.value) :
2219        window.webkitCancelRequestAnimationFrame ? window.webkitCancelRequestAnimationFrame(handle.value) : /* Support for legacy API */
2220        window.mozCancelRequestAnimationFrame ? window.mozCancelRequestAnimationFrame(handle.value) :
2221        window.oCancelRequestAnimationFrame	? window.oCancelRequestAnimationFrame(handle.value) :
2222        window.msCancelRequestAnimationFrame ? window.msCancelRequestAnimationFrame(handle.value) :
2223        clearTimeout(handle);
2224    };    
2225    
2226    
2227    
2228    /*
2229    ** Global data for this nanogallery2 instance
2230    **/
2231    var G=this;
2232    G.I =                       [];           // gallery items
2233    G.Id =                      [];           // gallery items
2234    G.O =                       null;         // user options
2235    G.baseEltID =               null;         // ID of the base element
2236    G.$E = { 
2237      base:                     null,         // base element
2238      conTnParent:              null,         // $g_containerThumbnailsParent
2239      conLoadingB:              null,         // loading bar - nanoGalleryLBarOff
2240      conConsole:               null,         // console for error messages
2241      conNavigationBar:         null,         // gallery navigation bar
2242      conTnBottom:              null,         // container on the bottom of the gallery
2243			scrollableParent:					null					// first scrollable parent container
2244    };
2245    G.shoppingCart =            [];
2246    G.layout = {                              // Layout informations
2247      internal :                true,
2248      engine :                  '',
2249      support :                 { rows: false },
2250      prerequisite :            { imageSize: false },
2251      SetEngine: function() {
2252
2253      if( G.layout.internal ) {
2254          if( G.tn.settings.width[G.GOM.curNavLevel][G.GOM.curWidth] == 'auto' || G.tn.settings.width[G.GOM.curNavLevel][G.GOM.curWidth] == '' ) {
2255            // do not use getH() / getW() here!
2256            G.layout.engine = 'JUSTIFIED';
2257            G.layout.support.rows = true;
2258            G.layout.prerequisite.imageSize = true;
2259            return;
2260          }
2261          if( G.tn.settings.height[G.GOM.curNavLevel][G.GOM.curWidth] == 'auto' || G.tn.settings.height[G.GOM.curNavLevel][G.GOM.curWidth] == '' ) {
2262            // do not use getH() / getW() here!
2263            G.layout.engine = 'CASCADING';
2264            G.layout.support.rows = false;
2265            G.layout.prerequisite.imageSize = true;
2266            return;
2267          }
2268
2269          if( G.tn.settings.getMosaic() != null ) {
2270            G.layout.engine = 'MOSAIC';
2271            G.layout.support.rows = true;
2272            G.layout.prerequisite.imageSize = false;
2273            return;
2274          }
2275
2276          G.layout.engine = 'GRID';
2277          G.layout.support.rows=true;
2278          // if( G.tn.opt.Get('crop') === true ) {
2279            // G.layout.prerequisite.imageSize = true;
2280          // }
2281          // else {
2282            G.layout.prerequisite.imageSize = false;
2283          // }
2284        }
2285      }
2286    };
2287    G.galleryResizeEventEnabled = false;
2288    G.galleryMaxRows = { l1: 0, lN: 0,
2289      Get: function() {
2290        return G.galleryMaxRows[G.GOM.curNavLevel];
2291      }
2292    };
2293    G.galleryMaxItems = { l1: 0, lN: 0,
2294      Get: function() {
2295        return G.galleryMaxItems[G.GOM.curNavLevel];
2296      }
2297    };
2298    G.galleryFilterTags = { l1: 0, lN: 0,
2299      Get: function() {
2300        return G.galleryFilterTags[G.GOM.curNavLevel];
2301      }
2302    };
2303    G.galleryFilterTagsMode = { l1: 0, lN: 0,
2304      Get: function() {
2305        return G.galleryFilterTagsMode[G.GOM.curNavLevel];
2306      }
2307    };
2308    G.galleryDisplayMode = { l1: 'FULLCONTENT', lN: 'FULLCONTENT',
2309      Get: function() {
2310        return G.galleryDisplayMode[G.GOM.curNavLevel];
2311      }
2312    };
2313    G.galleryLastRowFull = { l1: false, lN: false,
2314      Get: function() {
2315        return G.galleryLastRowFull[G.GOM.curNavLevel];
2316      }
2317    };
2318    G.gallerySorting = { l1: '', lN: '',
2319      Get: function() {
2320        return G.gallerySorting[G.GOM.curNavLevel];
2321      }
2322    };
2323    G.galleryDisplayTransition = { l1: 'none', lN: 'none',
2324      Get: function() {
2325        return G.galleryDisplayTransition[G.GOM.curNavLevel];
2326      }
2327    };
2328    G.galleryDisplayTransitionDuration = { l1: 500, lN: 500,
2329      Get: function() {
2330        return G.galleryDisplayTransitionDuration[G.GOM.curNavLevel];
2331      }
2332    };
2333    G.$currentTouchedThumbnail = null;    
2334    
2335    
2336    
2337    
2338    // ##### GENERAL THUMBNAILS PROPERTIES -->
2339    G.tn = {
2340      // levell specific options
2341      opt:  {
2342        l1: { crop: true, stacks: 0, stacksTranslateX: 0, stacksTranslateY: 0, stacksTranslateZ: 0, stacksRotateX: 0, stacksRotateY: 0, stacksRotateZ: 0, stacksScale: 0, borderHorizontal: 0, borderVertical: 0, baseGridHeight: 0, displayTransition: 'FADEIN', displayTransitionStartVal: 0, displayTransitionEasing: 'easeOutQuart', displayTransitionDuration: 240, displayInterval: 15 },
2343        lN: { crop: true, stacks: 0, stacksTranslateX: 0, stacksTranslateY: 0, stacksTranslateZ: 0, stacksRotateX: 0, stacksRotateY: 0, stacksRotateZ: 0, stacksScale: 0, borderHorizontal: 0, borderVertical: 0, baseGridHeight: 0, displayTransition: 'FADEIN', displayTransitionStartVal: 0, displayTransitionEasing: 'easeOutQuart', displayTransitionDuration: 240, displayInterval: 15 },
2344        Get: function(opt) {
2345          return G.tn.opt[G.GOM.curNavLevel][opt];
2346        }
2347      },
2348      scale:                          1,         // image scale depending of the hover effect
2349      labelHeight: {                  // in case label on bottom, otherwise always=0
2350        l1: 0, lN: 0,
2351        get: function() {
2352          return G.tn.labelHeight[G.GOM.curNavLevel];
2353        }
2354      },
2355      defaultSize: {                  // default thumbnail size
2356                                      // label height is not included
2357        width: {  l1 : { xs:0, sm:0, me:0, la:0, xl:0 }, lN : { xs:0, sm:0, me:0, la:0, xl:0 } },
2358        height: { l1 : { xs:0, sm:0, me:0, la:0, xl:0 }, lN : { xs:0, sm:0, me:0, la:0, xl:0 } },
2359        getWidth: function() {
2360          return G.tn.defaultSize.width[G.GOM.curNavLevel][G.GOM.curWidth];
2361        },
2362        getOuterWidth: function() {     // width including border
2363      G.tn.borderWidth = G.tn.opt.Get('borderHorizontal');
2364      G.tn.borderHeight = G.tn.opt.Get('borderVertical');
2365          var w = G.tn.defaultSize.width[G.GOM.curNavLevel][G.GOM.curWidth] + G.tn.opt.Get('borderHorizontal') * 2;
2366          if( G.O.thumbnailLabel.get('position') == 'right' || G.O.thumbnailLabel.get('position') == 'left' ) {
2367            w += G.tn.defaultSize.width[G.GOM.curNavLevel][G.GOM.curWidth];
2368          }
2369          return w;
2370        },
2371        getHeight: function() {
2372          return G.tn.defaultSize.height[G.GOM.curNavLevel][G.GOM.curWidth];
2373        },
2374        getOuterHeight: function() {     // height, border included
2375          return G.tn.defaultSize.height[G.GOM.curNavLevel][G.GOM.curWidth]+G.tn.opt.Get('borderVertical')*2;
2376        }
2377      },
2378      settings: {                     // user defined width/height of the image to display depending on the screen size
2379        width: {  l1 : { xs:0, sm:0, me:0, la:0, xl:0, xsc:'u', smc:'u', mec:'u', lac:'u', xlc:'u' },
2380                  lN : { xs:0, sm:0, me:0, la:0, xl:0, xsc:'u', smc:'u', mec:'u', lac:'u', xlc:'u' } },
2381        height: { l1 : { xs:0, sm:0, me:0, la:0, xl:0, xsc:'u', smc:'u', mec:'u', lac:'u', xlc:'u' }, 
2382                  lN : { xs:0, sm:0, me:0, la:0, xl:0, xsc:'u', smc:'u', mec:'u', lac:'u', xlc:'u' } },
2383        getH: function(l, w) {
2384          var cl = (l == undefined ? G.GOM.curNavLevel : l);
2385          var cw = (w == undefined ? G.GOM.curWidth : w);
2386          if( G.layout.engine == 'MOSAIC' ) {
2387          return this.height[cl][cw] * this.mosaic[cl+'Factor']['h'][cw];
2388          }
2389          else {
2390            return this.height[cl][cw];
2391          }
2392        },
2393        getW: function(l, w) {
2394          var cl = (l == undefined ? G.GOM.curNavLevel : l);
2395          var cw = (w == undefined ? G.GOM.curWidth : w);
2396          if( G.layout.engine == 'MOSAIC' ) {
2397            return this.width[cl][cw] * this.mosaic[cl+'Factor']['w'][cw];
2398          }
2399          else {
2400            return this.width[cl][cw];
2401            // return G.tn.settings.width[G.GOM.curNavLevel][G.GOM.curWidth];
2402          }
2403        },
2404        mosaic: { l1 : { xs: null, sm: null, me: null, la: null, xl: null },
2405                  lN : { xs: null, sm: null, me: null, la: null, xl: null },
2406                  l1Factor : { h :{ xs: 1, sm: 1, me: 1, la: 1, xl: 1 }, w :{ xs: 1, sm: 1, me: 1, la: 1, xl: 1 }},
2407                  lNFactor : { h :{ xs: 1, sm: 1, me: 1, la: 1, xl: 1 }, w :{ xs: 1, sm: 1, me: 1, la: 1, xl: 1 }}
2408                  },
2409				getMosaic: function() {
2410          return this.mosaic[G.GOM.curNavLevel][G.GOM.curWidth];
2411        },
2412        mosaicCalcFactor: function(l, w) {
2413            // retrieve max size multiplicator
2414            var maxW = 1;
2415            var maxH = 1;
2416            for( var n = 0; n < G.tn.settings.mosaic[l][w].length; n++ ) {
2417              maxW = Math.max(maxW, this.mosaic[l][w][n]['w']);
2418              maxH = Math.max(maxH, this.mosaic[l][w][n]['h']);
2419            }
2420            this.mosaic[l + 'Factor']['h'][w] = maxH;
2421            this.mosaic[l + 'Factor']['w'][w] = maxW;
2422        },
2423        gutterHeight: { l1 : { xs:0, sm:0, me:0, la:0, xl:0 }, lN : { xs:0, sm:0, me:0, la:0, xl:0 } },
2424        gutterWidth:  { l1 : { xs:0, sm:0, me:0, la:0, xl:0 }, lN : { xs:0, sm:0, me:0, la:0, xl:0 } },
2425        GetResponsive: function( setting ) {
2426          return this[setting][G.GOM.curNavLevel][G.GOM.curWidth];
2427        }
2428      },
2429      // thumbnail hover effects
2430      hoverEffects : {
2431        std :   [],
2432        level1: [],
2433        get: function() {
2434          if( G.GOM.curNavLevel == 'l1' && this.level1.length !== 0 ) {
2435            return this.level1;
2436          }
2437          else {
2438            return this.std;
2439          }
2440        }
2441      },
2442      // thumbnail init
2443      buildInit : {
2444        std :   [],
2445        level1: [],
2446        get: function() {
2447          if( G.GOM.curNavLevel == 'l1' && this.level1.length !== 0 ) {
2448            return this.level1;
2449          }
2450          else {
2451            return this.std;
2452          }
2453        }
2454      },
2455      // thumbnail toolbars
2456      toolbar: {
2457        album :   { topLeft : '', topRight: '', bottomLeft: '', bottomRight: '' },
2458        image :   { topLeft : '', topRight: '', bottomLeft: '', bottomRight: '' },
2459        albumUp : { topLeft : '', topRight: '', bottomLeft: '', bottomRight: '' },
2460        get: function( item ) {
2461          return this[item.kind];
2462        },
2463      },
2464      style: {
2465        // inline CSS
2466        l1 : { label: '', title: '', desc: '' },
2467        lN : { label: '', title: '', desc: '' },
2468        getTitle : function() {
2469          return ('style="' + this[G.GOM.curNavLevel].title + '"');
2470        },
2471        getDesc : function() {
2472          return ('style="' + this[G.GOM.curNavLevel].desc + '"');
2473        },
2474        getLabel: function() {
2475          var s='style="'+ this[G.GOM.curNavLevel].label;
2476          s+= (G.O.RTL ? '"direction:RTL;"' :'');
2477          s+='"';
2478          return s;
2479        }
2480      }
2481    };
2482    G.scrollTimeOut =             0;
2483    G.i18nTranslations =          {'paginationPrevious':'Previous', 'paginationNext':'Next', 'breadcrumbHome':'List of Albums', 'thumbnailImageTitle':'', 'thumbnailAlbumTitle':'', 'thumbnailImageDescription':'', 'thumbnailAlbumDescription':'' };
2484    G.emptyGif =                  'data:image/gif;base64,R0lGODlhEAAQAIAAAP///////yH5BAEKAAEALAAAAAAQABAAAAIOjI+py+0Po5y02ouzPgUAOw==';
2485    G.CSStransformName =          FirstSupportedPropertyName(["transform", "msTransform", "MozTransform", "WebkitTransform", "OTransform"]);
2486    // G.CSSfilterName =          FirstSupportedPropertyName(["filter", "WebkitFilter"]);
2487    G.CSStransformStyle =         FirstSupportedPropertyName(["transformStyle", "msTransformStyle", "MozTransformStyle", "WebkitTransformStyle", "OTransformStyle"]);
2488    G.CSSperspective =            FirstSupportedPropertyName(["perspective", "msPerspective", "MozPerspective", "WebkitPerspective", "OPerspective"]);
2489    G.CSSbackfaceVisibilityName = FirstSupportedPropertyName(["backfaceVisibility", "msBackfaceVisibility", "MozBackfaceVisibility", "WebkitBackfaceVisibility", "OBackfaceVisibility"]);
2490    G.CSStransitionName =         FirstSupportedPropertyName(["transition", "msTransition", "MozTransition", "WebkitTransition", "OTransition"]);
2491    G.CSSanimationName =          FirstSupportedPropertyName(["animation", "msAnimation", "MozAnimation", "WebkitAnimation", "OAnimation"]);
2492    G.GalleryResizeThrottled =    throttle(GalleryResize, 15, {leading: false});
2493    
2494    G.blockList =                 null;     // album names - block list
2495    G.allowList =                 null;     // album names - allow list
2496    G.albumList =                 [];       // album list
2497    G.locationHashLastUsed =      '';
2498    G.custGlobals =               {};
2499    G.touchAutoOpenDelayTimerID = 0;
2500    G.i18nLang =                  '';
2501    G.timeLastTouchStart =        0;
2502    G.custGlobals =               {};
2503    G.markupOrApiProcessed =      false;
2504    
2505    //------------------------
2506    //--- Gallery Object Model
2507    G.GOM = {
2508      albumIdx :                  -1, // index (in G.I) of the currently displayed album
2509      clipArea :                  { top: 0, height: 0 }, // area of the GOM to display on screen
2510      displayArea :               { width: 0 , height: 0 }, // size of the GOM area (=used area, not available area)
2511      displayAreaLast :           { width: 0 , height: 0 }, // previous size of the GOM area
2512      displayedMoreSteps :        0,  // current number of displayed steps (moreButton mode)
2513      items:                      [], // current items of the GOMS
2514      $imgPreloader:              [],
2515      thumbnails2Display:         [],
2516      itemsDisplayed :            0, // number of currently displayed thumbnails
2517      firstDisplay :              true,
2518      firstDisplayTime :          0,      // in conjunction with galleryRenderDelay
2519      navigationBar : {           // content of the navigation bar (for breadcrumb, filter tags and next/previous pagination)
2520        displayed:                false,
2521        $newContent:              ''
2522      },
2523      cache : {                   // cached data
2524        viewport:                 null,
2525        containerOffset:          null,
2526        areaWidth:                100         // available area width
2527      },
2528      nbSelected :                0,        // number of selected items
2529      pagination :                { currentPage: 0 }, // pagination data
2530      panThreshold:               60,       // threshold value (in pixels) to block horizontal pan/swipe
2531      panYOnly:                   false,    // threshold value reach -> definitively block horizontal pan until end of pan
2532      lastFullRow :               -1,       // number of the last row without holes
2533      lastDisplayedIdx:           -1,       // used to display the counter of not displayed items
2534      displayInterval :           { from: 0, len: 0 },
2535      hammertime:                 null,
2536      curNavLevel:                'l1',   // current navigation level (l1 or LN)
2537      curWidth:                   'me',
2538      albumSearch:                '',     // current search string -> title (used to filter the thumbnails on screen)
2539      albumSearchTags:            '',     // current search string -> tags
2540      lastZIndex:                 0,      // used to put a thumbnail on top of all others (for exemple for scale hover effect)
2541      lastRandomValue:            0,
2542      slider : {                          // slider on last thumbnail
2543        hostIdx:                  -1,       // idx of the thumbnail hosting the slider
2544        hostItem:                 null,     // item hosting the slider
2545        currentIdx:               0,        // idx of the current displayed item
2546        nextIdx:                  0,        // idx of the next item to display in the slider
2547        timerID:                  0,
2548        tween:                    null      // tranistion tween instance
2549      },
2550      NGY2Item: function( idx ) {   // returns a NGY2Item or null if it does not exist
2551        if( G.GOM.items[idx] == undefined || G.GOM.items[idx] == null ) { return null; }
2552        var i = G.GOM.items[idx].thumbnailIdx;
2553        return G.I[i];
2554      },
2555      // One GOM item (thumbnail)
2556      // function GTn(index, width, height) {
2557      GTn: function(index, width, height) {
2558        this.thumbnailIdx = index;
2559        this.width =                0;      // thumbnail width
2560        this.height =               0;      // thumbnail height
2561        this.top =                  0;      // position: top
2562        this.left =                 0;      // position: left
2563        this.row =                  0;      // position: row number
2564        this.imageWidth =           width;  // image width
2565        this.imageHeight =          height; // image height
2566        this.resizedContentWidth =  0;
2567        this.resizedContentHeight = 0;
2568        this.displayed =            false;
2569        this.neverDisplayed =       true;
2570        this.inDisplayArea =        false;
2571      },
2572      // Position the top of the gallery to make it visible, if not displayed
2573      ScrollToTop: function() {
2574
2575        if( G.GOM.firstDisplay ) {      
2576          // do no scroll to top on first display
2577          return;
2578        }
2579        
2580        if( G.$E.scrollableParent === null && !topInViewportVert(G.$E.base, 20) ) {
2581          // $('html, body').animate({scrollTop: G.$E.base.offset().top}, 200);
2582          G.$E.base.get(0).scrollIntoView();
2583        }
2584        
2585        if( G.$E.scrollableParent !== null ) {
2586          // gallery in a scrollable container: check if we have to scroll up so that the top of the gallery is visible
2587          // vertical position of the scrollbar
2588          var scrollTop = G.$E.scrollableParent.scrollTop();
2589          // top of the gallery relative to the top of the scrollable container
2590          var dist = Math.abs(G.$E.scrollableParent.offset().top - G.$E.base.offset().top - scrollTop);
2591          if( scrollTop > dist ) {
2592            window.ng_draf( function() {
2593              // we need a little delay before setting the new scrollbar (but why?....)
2594               G.$E.scrollableParent.scrollTop(dist);
2595            });
2596          }
2597        }
2598      }
2599    };
2600    
2601    
2602    //------------------------
2603    //--- Viewer Object Model
2604    
2605    G.VOM = {
2606      viewerDisplayed:            false,  // is the viewer currently displayed
2607      viewerIsFullscreen:         false,  // viewer in fullscreen mode
2608      infoDisplayed:              false,  // is the info box displayed
2609      toolbarsDisplayed:          true,   // the toolbars are displayed
2610      toolsHide:                  null,
2611      zoom : {
2612        posX:                     0,      // position to center zoom in/out
2613        posY:                     0,
2614        userFactor:               1,      // user zoom factor (applied to the baseZoom factor)
2615        isZooming:                false
2616      },
2617      padding:                    { H: 0, V: 0 }, // padding for the image
2618      window:                     { lastWidth: 0, lastHeight: 0 },
2619      $viewer:                    null,
2620      $toolbar:                   null,   // viewerToolbar
2621      $toolbarTL:                 null,   // viewer toolbar on top left
2622      $toolbarTR:                 null,   // viewer toolbar on top right
2623      
2624      toolbarMode:                'std',  // current toolbar mode (standard, minimized)
2625      playSlideshow :             false,  // slide show mode status
2626      playSlideshowTimerID:       0,      // slideshow mode time
2627      slideshowDelay:             3000,   // slideshow mode - delay before next image
2628      albumID:                    -1,
2629      viewerMediaIsChanged:       false,  // media display is currently modified
2630      items:                      [],     // current list of images to be managed by the viewer
2631
2632      panMode:                    'off',  // if panning, which element -> media, gallery, or zoom - if not -> off
2633
2634      $baseCont:                  null,   // lightbox container
2635      $content:                   null,   // pointer to the 3 media in the viewer
2636      content: {
2637        previous : {
2638            vIdx: -1,
2639            $media: null,
2640            NGY2Item: function() {
2641              return G.I[ G.VOM.items[G.VOM.content.previous.vIdx].ngy2ItemIdx ];
2642            }
2643        },
2644        current : {
2645            vIdx: -1,
2646            $media: null,
2647            NGY2Item: function() {
2648              return G.I[ G.VOM.items[G.VOM.content.current.vIdx].ngy2ItemIdx ];
2649            }
2650        },
2651        next : {
2652            vIdx: -1,
2653            $media: null,
2654            NGY2Item: function() {
2655              return G.I[ G.VOM.items[G.VOM.content.next.vIdx].ngy2ItemIdx ];
2656            }
2657        }
2658      },
2659      IdxNext: function() {
2660        var n = 0;
2661        // if( G.VOM.currItemIdx <= (G.VOM.items.length-1) ) {
2662        if( G.VOM.content.current.vIdx < (G.VOM.items.length-1) ) {
2663          n = G.VOM.content.current.vIdx + 1;
2664        }
2665        return n;
2666      },
2667      IdxPrevious: function() {
2668        var n = G.VOM.content.current.vIdx - 1;
2669        if( G.VOM.content.current.vIdx == 0 ) {
2670          n = G.VOM.items.length - 1;
2671        }
2672        return n;
2673      },
2674			
2675			gallery: {
2676        $elt: null,             // Base container
2677        $tmbCont: null,         // Thumbnail container
2678				gwidth: 0,              // thumbnail container width (all thumbnails)
2679        vwidth: 0,              // visible width of the gallery (just for the visible thumbnails)
2680				oneTmbWidth: 0,
2681				firstDisplay: true,
2682				posX: 0,
2683				SetThumbnailActive() {
2684					if( G.O.viewerGallery == 'none' ) { return; }
2685					this.$tmbCont.children().removeClass('activeVThumbnail');
2686					this.$tmbCont.children().eq( G.VOM.content.current.vIdx ).addClass('activeVThumbnail');
2687					this.firstDisplay = false;
2688				},
2689				Resize: function() {
2690					if( G.O.viewerGallery == 'none' ) { return; }
2691
2692					if( !this.firstDisplay ) {
2693						var viewerW = G.VOM.$viewer.width();
2694						
2695						// Center base element 
2696						var maxTmb = Math.trunc(viewerW / this.oneTmbWidth);      // max thumbnail that can be displayed
2697            this.vwidth = maxTmb * this.oneTmbWidth;
2698						this.$elt.css({ width: this.vwidth, left: (viewerW - this.vwidth)/2 });
2699            
2700						// Set the position the thumbnails container (if there's no enough space for all thumbnails)
2701						if( G.VOM.items.length >= maxTmb ) {
2702							var tmbPos = this.oneTmbWidth * G.VOM.content.current.vIdx;    // left position of the selected thumbnail
2703              
2704              if( (tmbPos + this.posX) < this.vwidth ) {
2705                  if( tmbPos + this.posX < 0 ) {
2706                    this.posX = -tmbPos;
2707                  }
2708              }
2709              else {
2710                if( tmbPos + this.posX >= this.vwidth ) {
2711                  this.posX = this.vwidth - (tmbPos + this.oneTmbWidth)
2712                }
2713              }
2714						}
2715						
2716            this.PanGallery(0);
2717					}
2718          else {
2719            // first display of the gallery -> opacity transition
2720            new NGTweenable().tween({
2721              from:         { opacity: 0 },
2722              to:           { opacity: 1 },
2723              easing:       'easeInOutSine',
2724              duration:     1000,
2725              step:         function (state) {
2726                // G.VOM.gallery.$elt.css( state );
2727              },
2728              finish:       function (state) {
2729                // G.VOM.gallery.$elt.css({ opacity: 1});
2730              }
2731            });
2732
2733          }
2734				},
2735        PanGallery: function( panX ){
2736          
2737          // all thumbnails are visible -> center the base element
2738          if( this.gwidth < G.VOM.$viewer.width() ) {       // this.oneTmbWidth
2739            this.posX = (G.VOM.$viewer.width() - this.gwidth) / 2;   
2740            panX = 0;   // block pan
2741          }
2742          
2743          // if( this.posX > (this.vwidth - this.oneTmbWidth) ) {
2744          if( this.posX > (this.vwidth - this.oneTmbWidth) ) {
2745            // gallery is outside of the screen, right side
2746            this.posX = this.vwidth - this.oneTmbWidth;
2747          }
2748          if( (this.posX+this.gwidth) < this.oneTmbWidth ) {
2749            // gallery is outside of the screen, left side
2750            this.posX = -this.gwidth + this.oneTmbWidth;
2751          }
2752          
2753          this.$tmbCont.css( G.CSStransformName , 'translateX(' + (this.posX + panX) + 'px)');
2754        },
2755        PanGalleryEnd: function( velocity ) {      // velocity = pixels/millisecond
2756
2757          var d = velocity * 100;         // distance 
2758          new NGTweenable().tween({
2759            from:         { pan: G.VOM.gallery.posX },
2760            to:           { pan: G.VOM.gallery.posX + d },
2761            easing:       'easeOutQuad',
2762            duration:     500,
2763            step:         function (state) {
2764              G.VOM.gallery.posX = state.pan;
2765              G.VOM.gallery.PanGallery( 0 );
2766            }
2767          });
2768        }
2769				
2770			},
2771      hammertime:         null,   // hammer.js manager
2772      swipePosX:          0,      // current horizontal swip position
2773      panPosX:            0,      // position for manual pan
2774      panPosY:            0,
2775      panThreshold:       60,     // threshold value (in pixels) to block vertical pan
2776      panXOnly:           false,  // threshold value reach -> definitively block vertical pan until end of pan
2777      singletapTime:			0,
2778			viewerTheme:        '',
2779      timeImgChanged:     0,
2780      ImageLoader: {
2781        // fires a callback when image size is know (during download)
2782        // inspired by ROB - http://stackoverflow.com/users/226507/rob
2783        maxChecks:        1000,
2784        list:             [],
2785        intervalHandle :  null,
2786
2787        loadImage : function (callback, ngitem) {
2788          if( ngitem.mediaKind != 'img' ) { return; }     // ignore - only for images
2789          var img = new Image ();
2790          img.src = ngitem.responsiveURL();
2791          if (img.width && img.height) {
2792            callback (img.width, img.height, ngitem, 0);
2793            }
2794          else {
2795            var obj = {image: img, url: ngitem.responsiveURL(), ngitem: ngitem, callback: callback, checks: 1};
2796            var i;
2797            for (i=0; i < this.list.length; i++)    {
2798              if (this.list[i] == null)
2799                break;
2800              }
2801            this.list[i] = obj;
2802            if (!this.intervalHandle)
2803              this.intervalHandle = setInterval(this.interval, 50);
2804            }
2805          },
2806
2807        // called by setInterval  
2808        interval : function () {
2809          var count = 0;
2810          var list = G.VOM.ImageLoader.list, item;
2811          for (var i=0; i<list.length; i++) {
2812            item = list[i];
2813            if (item != null) {
2814              if (item.image.width && item.image.height) {
2815                G.VOM.ImageLoader.list[i] = null;
2816                item.callback (item.image.width, item.image.height, item.ngitem, item.checks);
2817                }
2818              else if (item.checks > G.VOM.ImageLoader.maxChecks) {
2819                G.VOM.ImageLoader.list[i] = null;
2820                item.callback (0, 0, item.ngitem, item.checks);
2821                }
2822              else {
2823                count++;
2824                item.checks++;
2825                }
2826              }
2827            }
2828          if (count == 0) {
2829            G.VOM.ImageLoader.list = [];
2830            clearInterval (G.VOM.ImageLoader.intervalHandle);
2831            delete G.VOM.ImageLoader.intervalHandle;
2832            }
2833          }
2834        }
2835    }
2836    // One VOM item (image)
2837    function VImg( index ) {
2838      this.$e = null;
2839      this.ngy2ItemIdx = index;
2840      this.mediaNumber = G.VOM.items.length + 1;     
2841      this.posX = 0;    // to center the element
2842      this.posY = 0;
2843    }
2844    
2845    
2846    //------------------------
2847    //--- popup
2848    G.popup = {
2849      isDisplayed:      false,
2850      $elt:             null,
2851      close: function() {
2852        if( this.$elt != null ) {
2853          var tweenable = new NGTweenable();
2854          tweenable.tween({
2855            from:       { opacity:1  },
2856            to:         { opacity:0 },
2857            attachment: { t: this },
2858            easing:     'easeInOutSine',
2859            duration:   100,
2860            step: function (state, att) {
2861              if( att.t.$elt != null ) {
2862                att.t.$elt.css('opacity',state.opacity);
2863              }
2864            },
2865            finish: function (state, att) {
2866              if( att.t.$elt != null ) {
2867                att.t.$elt.remove();
2868                att.t.$elt=null;
2869              }
2870              att.t.isDisplayed=false;
2871            }
2872          });
2873        }
2874      }
2875    }
2876    
2877    
2878    // Color schemes - Gallery
2879    // Gradient generator: https://www.grabient.com/
2880    G.galleryTheme_dark = {
2881      navigationBar :         { background: 'none', borderTop: '', borderBottom: '', borderRight: '', borderLeft: '' },
2882      navigationBreadcrumb :  { background: '#111', color: '#fff', colorHover: '#ccc', borderRadius: '4px' },
2883      navigationFilter :      { color: '#ddd', background: '#111', colorSelected: '#fff', backgroundSelected: '#111', borderRadius: '4px' },
2884      navigationPagination :  { background: '#111', color: '#fff', colorHover: '#ccc', borderRadius: '4px' },
2885      thumbnail :             { background: '#444', backgroundImage: 'linear-gradient(315deg, #111 0%, #445 90%)', borderColor: '#000', borderRadius: '0px', labelOpacity : 1, labelBackground: 'rgba(34, 34, 34, 0)', titleColor: '#fff', titleBgColor: 'transparent', titleShadow: '', descriptionColor: '#ccc', descriptionBgColor: 'transparent', descriptionShadow: '', stackBackground: '#aaa' },
2886      thumbnailIcon :         { padding: '5px', color: '#fff', shadow:'' },
2887      pagination :            { background: '#181818', backgroundSelected: '#666', color: '#fff', borderRadius: '2px', shapeBorder: '3px solid #666', shapeColor: '#444', shapeSelectedColor: '#aaa'}
2888    };
2889
2890    G.galleryTheme_light = {
2891      navigationBar :         { background: 'none', borderTop: '', borderBottom: '', borderRight: '', borderLeft: '' },
2892      navigationBreadcrumb :  { background: '#eee', color: '#000', colorHover: '#333', borderRadius: '4px' },
2893      navigationFilter :      { background: '#eee', color: '#222', colorSelected: '#000', backgroundSelected: '#eee', borderRadius: '4px' },
2894      navigationPagination :  { background: '#eee', color: '#000', colorHover: '#333', borderRadius: '4px' },
2895      thumbnail :             { background: '#444', backgroundImage: 'linear-gradient(315deg, #111 0%, #445 90%)', borderColor: '#000', labelOpacity : 1, labelBackground: 'rgba(34, 34, 34, 0)', titleColor: '#fff', titleBgColor: 'transparent', titleShadow: '', descriptionColor: '#ccc', descriptionBgColor: 'transparent', descriptionShadow: '', stackBackground: '#888' },
2896      thumbnailIcon :         { padding: '5px', color: '#fff' },
2897      pagination :            { background: '#eee', backgroundSelected: '#aaa', color: '#000', borderRadius: '2px', shapeBorder: '3px solid #666', shapeColor: '#444', shapeSelectedColor: '#aaa'}
2898    };
2899
2900    // Color schemes - lightbox
2901    G.viewerTheme_dark = {
2902      background:             '#000',
2903      barBackground:          'rgba(4, 4, 4, 0.2)',
2904      barBorder:              '0px solid #111',
2905      barColor:               '#fff',
2906      barDescriptionColor:    '#ccc'
2907    };
2908    G.viewerTheme_light = {
2909      background:             '#f8f8f8',
2910      barBackground:          'rgba(4, 4, 4, 0.7)',
2911      barBorder:              '0px solid #111',
2912      barColor:               '#fff',
2913      barDescriptionColor:    '#ccc'
2914    };
2915
2916      
2917    
2918    // shortcut with G context to NGY2TOOLS
2919    // var NanoAlert =           NGY2Tools.NanoAlert.bind(G);
2920    // var NanoConsoleLog =      NGY2Tools.NanoConsoleLog.bind(G);
2921    var NanoAlert =           NGY2Tools.NanoAlert;
2922    var NanoConsoleLog =      NGY2Tools.NanoConsoleLog;
2923
2924    
2925    /** @function initiateGallery2 */
2926    this.initiateGallery2 = function( element, params ) {
2927
2928      // GLOBAL OPTIONS
2929      G.O =               params;
2930      // Base element
2931      G.$E.base =         jQuery(element);
2932      G.baseEltID =       G.$E.base.attr('id');
2933      if( G.baseEltID == undefined ) {
2934        // set a default ID to the root container
2935        var base_id = 'my_nanogallery';
2936        var c = '';
2937        var f = true;
2938        while( f ) {
2939          if (document.getElementById(base_id + c)) {
2940            // ID already exists
2941            if( c == '' ) {
2942              c = 1;
2943            }
2944            else {
2945              c++;
2946            }
2947          }
2948          else {
2949            f = false;
2950            G.baseEltID = 'my_nanogallery' + c;
2951          }
2952        }
2953        G.$E.base.attr('id', G.baseEltID)
2954      }
2955      G.O.$markup =       [];
2956      
2957      DefineVariables();
2958      SetPolyFills();
2959      BuildSkeleton();
2960      G.GOM.firstDisplayTime = Date.now();
2961      
2962      SetGlobalEvents();
2963
2964      // check if only one specific album will be used
2965      if( !G.O.lightboxStandalone ) {
2966				var albumToDisplay = G.O.album;
2967				if( albumToDisplay == '' && G.O.photoset != '' ) {
2968					albumToDisplay = G.O.photoset;
2969					G.O.album = G.O.photoset;
2970				}
2971				if( albumToDisplay != '' ) {
2972					G.O.displayBreadcrumb = false;    // no breadcrumb since only 1 album
2973					if( albumToDisplay.toUpperCase() != 'NONE' ) {
2974						// open a public album
2975						if( G.O.kind == "nano_photos_provider2") {
2976							if( albumToDisplay == decodeURIComponent(albumToDisplay)) {
2977								// album ID must be encoded
2978								albumToDisplay = encodeURIComponent(albumToDisplay);
2979								G.O.album = albumToDisplay;
2980							}
2981						}
2982						NGY2Item.New( G, '', '', albumToDisplay, '-1', 'album' );
2983						if( !ProcessLocationHash() ) {
2984							DisplayAlbum('-1', albumToDisplay);
2985						}
2986						return;
2987					}
2988				}
2989      }
2990     
2991      // use full content
2992      // add base album
2993      NGY2Item.New( G, G.i18nTranslations.breadcrumbHome, '', '0', '-1', 'album' );
2994
2995      processStartOptions();
2996
2997    }
2998
2999
3000    /** @function processStartOptions */
3001    function processStartOptions() {
3002      // open image or album
3003      // 1. load hidden albums
3004      // 2. check if location hash set (deep linking)
3005      // 3. check openOnStart parameter
3006      // 4. open root album (ID=-1)
3007
3008      // hidden/private albums are loaded on plugin start (Picasa) --> no more available in Google Photos
3009      // if( G.albumListHidden.length > 0 ) {
3010      //  jQuery.nanogallery2['data_'+G.O.kind](G, 'GetHiddenAlbums', G.albumListHidden, processStartOptionsPart2);
3011      //  return;
3012      //}
3013      
3014      if( !ProcessLocationHash() ) {
3015        processStartOptionsPart2();
3016      }
3017    }
3018
3019    /** @function processStartOptionsPart2 */
3020    function processStartOptionsPart2() {
3021  
3022      // Check location hash + start parameters -> determine what to do on start
3023      if( G.O.lightboxStandalone ) {
3024        LightboxStandaloneFindContent();
3025			}
3026			else {
3027				// openOnStart parameter
3028				if( G.O.openOnStart != '' ) {
3029					var IDs = parseIDs(G.O.openOnStart);
3030					if( IDs.imageID != '0' ) {
3031						DisplayPhoto(IDs.imageID, IDs.albumID);
3032					}
3033					else {
3034						DisplayAlbum('-1', IDs.albumID);
3035					}
3036				}
3037				else {
3038					// open root album (ID = -1)
3039					DisplayAlbum('-1', 0);
3040				}
3041      }
3042    }
3043    
3044    
3045    // Lightbox standaone -> retrieve the items to display
3046    // Each item needs at least a thumbnail image and a big image
3047    // ONLY IMAGES SUPPORTED
3048    function LightboxStandaloneFindContent() {
3049
3050      G.GOM.curNavLevel = 'l1';
3051
3052      if( G.O.items == null ) {
3053        // retrieve all element having "data-nanogallery2-lightbox" and from the same group if defined
3054        var elts = jQuery('[data-nanogallery2-Lightbox');
3055        // element group
3056        var g = G.$E.base[0].dataset.nanogallery2Lgroup;
3057
3058        GetContentMarkup( elts, g );
3059      }
3060      else {
3061        // Content defined in the starting parameters
3062        GetContentApiObject();
3063      }
3064      
3065      LightboxStandaloneDisplay();
3066        
3067    }
3068    
3069    
3070    // Populate G.VOM.items + open the lightbox
3071    function LightboxStandaloneDisplay() {
3072
3073      G.VOM.items = [];
3074      G.VOM.albumID = '0';
3075      G.GOM.curNavLevel = 'l1';
3076      var vcnt = 0;
3077
3078      var elt = G.$E.base[0].attributes;
3079      var thumbsrc = '';
3080      // src attribute (img element)
3081      if( elt.hasOwnProperty('src') ) {
3082        thumbsrc = elt['src'].nodeValue;
3083      }
3084      // data-ngthumb attribute
3085      if( thumbsrc == '' && elt.hasOwnProperty('data-ngthumb') ) {
3086        thumbsrc = elt['data-ngthumb'].nodeValue;
3087      }
3088      
3089      var displayIdx = undefined;
3090      for( var idx = 0; idx < G.I.length; idx++ ) {
3091        if( G.I[idx].kind == 'image' ) {
3092          var vimg = new VImg(idx);
3093          G.VOM.items.push(vimg);
3094
3095          if( G.I[idx].thumbImg().src == thumbsrc ) {
3096            // same thumbnail URL
3097            displayIdx = vcnt;
3098          }
3099          vcnt++;
3100        }
3101        
3102      }
3103      if( G.VOM.items.length > 0 ) {
3104        LightboxOpen( displayIdx );
3105      }
3106      else {
3107        NanoConsoleLog(G, 'No content for Lightbox standalone.');
3108      }
3109    }
3110   
3111    
3112    
3113    // Parse string to extract albumID and imageID (format albumID/imageID)
3114    function parseIDs( IDs ) {
3115      var r = { albumID: '0', imageID: '0' };
3116      
3117      var t = IDs.split('/');
3118      if( t.length > 0 ) {
3119        r.albumID = t[0];
3120        if( t.length > 1 ) {
3121          r.imageID = t[1];
3122        }
3123      }
3124      return r;
3125    }
3126    
3127
3128    /** @function DisplayAlbum */
3129    function DisplayAlbum( imageID, albumID ) {
3130      // close viewer if already displayed
3131      if( G.VOM.viewerDisplayed ) {
3132        LightboxClose(null);
3133      }
3134    
3135      // set current navigation level (l1 or lN)
3136      var albumIdx = NGY2Item.GetIdx(G, albumID);
3137      G.GOM.curNavLevel = 'lN';
3138      if( albumIdx == 0 ) {
3139        G.GOM.curNavLevel = 'l1';
3140      }
3141      G.layout.SetEngine();
3142      G.galleryResizeEventEnabled = false;
3143
3144      if( albumIdx == -1 ) {
3145        NGY2Item.New( G, '', '', albumID, '0', 'album' );    // create empty album
3146        albumIdx = G.I.length - 1;
3147      }
3148    
3149      if( !G.I[albumIdx].contentIsLoaded ) {
3150        // get content of the album if not already loaded
3151        AlbumGetContent( albumID, DisplayAlbum, imageID, albumID );
3152        return;
3153      }
3154    
3155      ThumbnailSelectionClear();
3156    
3157      G.GOM.pagination.currentPage = 0;
3158      SetLocationHash( albumID, '' );
3159      GalleryRender( albumIdx );
3160    
3161    }
3162
3163
3164    //----- manage the bottom area of the gallery -> "pagination" or "more button"
3165    function GalleryBottomManage() {
3166
3167      switch( G.galleryDisplayMode.Get() ) {
3168        case 'PAGINATION':
3169          if( G.layout.support.rows && G.galleryMaxRows.Get() > 0 ) {
3170            ManagePagination();
3171          }
3172          break;
3173        case 'MOREBUTTON':
3174          G.$E.conTnBottom.off('click');
3175          var nb = G.GOM.items.length-G.GOM.itemsDisplayed;
3176          if( nb == 0 ) {
3177            G.$E.conTnBottom.empty();
3178          }
3179          else {
3180            G.$E.conTnBottom.html('<div class="nGY2GalleryMoreButton"><div class="nGY2GalleryMoreButtonAnnotation">+'+nb+' ' + G.O.icons.galleryMoreButton +'</div></div>');
3181            G.$E.conTnBottom.on('click', function(e) {
3182              G.GOM.displayedMoreSteps++;
3183              GalleryResize();
3184            });
3185          }
3186          break;
3187        case 'FULLCONTENT':
3188        default:
3189          break;
3190      }
3191    }
3192    
3193    
3194    // add one album/folder to the breadcrumb
3195    function breadcrumbAdd( albumIdx ) {
3196
3197      var ic='';
3198      if( !G.O.breadcrumbHideIcons ) {
3199        ic=G.O.icons.breadcrumbAlbum;
3200        if( albumIdx == 0 ) {
3201          ic=G.O.icons.breadcrumbHome;
3202        }
3203      }
3204      var $newDiv =jQuery('<div class="oneItem">'+ic + G.I[albumIdx].title+'</div>').appendTo(G.GOM.navigationBar.$newContent.find('.nGY2Breadcrumb'));
3205      if( G.O.breadcrumbOnlyCurrentLevel ) {
3206        // link to parent folder (only 1 level is displayed in the breadcrumb)
3207        if( albumIdx == 0 ) {
3208          // no parent level -> stay on current one
3209          jQuery($newDiv).data('albumID','0');
3210        }
3211        else {
3212          jQuery($newDiv).data('albumID',G.I[albumIdx].albumID);
3213        }
3214      }
3215      else {
3216        // link to current folder
3217        jQuery($newDiv).data('albumID',G.I[albumIdx].GetID());
3218      }
3219      $newDiv.click(function() {
3220        var cAlbumID = jQuery(this).data('albumID');
3221        DisplayAlbum('-1', cAlbumID);
3222        return;
3223      });
3224    }
3225
3226    // add one separator to breadcrumb
3227    function breadcrumbAddSeparator( lastAlbumID ) {
3228      var $newSep=jQuery('<div class="oneItem">'+(G.O.RTL ? G.O.icons.breadcrumbSeparatorRtl : G.O.icons.breadcrumbSeparator)+'</div>').appendTo(G.GOM.navigationBar.$newContent.find('.nGY2Breadcrumb'));
3229      jQuery($newSep).data('albumIdx',lastAlbumID);
3230      $newSep.click(function() {
3231        var sepAlbumIdx=jQuery(this).data('albumIdx');
3232        DisplayAlbum('-1', G.I[sepAlbumIdx].GetID());
3233        return;
3234      });
3235    }
3236
3237    
3238    
3239    // Manage the gallery toolbar (breadcrumb + tag filter + pagination next/previous)
3240    function GalleryNavigationBar( albumIdx ) {
3241
3242      // Title + background image
3243      // var bgImage='';
3244      // var l=G.I.length;
3245      // var albumID = G.I[albumIdx].GetID();
3246      // for( var idx=0; idx<l ; idx++) {
3247        // var item=G.I[idx];
3248        // if( item.kind == 'image' && item.isToDisplay(albumID) ) {
3249          // bgImage='<div id="pipo" class="pipo" style="height: 150px; width:100%; background-image: url("' + item.responsiveURL() + '"); background-size: cover; background-position: center center; filter:blur(2px)">pipo</div>';
3250          // break;
3251        // }
3252      // }
3253
3254      //console.log(bgImage);
3255    
3256      // new navigation bar items are not build in the DOM, but in memory
3257      G.GOM.navigationBar.$newContent=jQuery('<div class="nGY2Navigationbar"></div>');
3258      //G.GOM.navigationBar.$newContent = jQuery(bgImage );
3259      //console.log(G.GOM.navigationBar.$newContent);
3260
3261      //-- manage breadcrumb
3262      if( G.O.displayBreadcrumb == true && !G.O.thumbnailAlbumDisplayImage) {
3263        // retrieve new folder level
3264        var newLevel = 0,
3265        lstItems=[];
3266        if( albumIdx != 0 ) {
3267          var l=G.I.length,
3268          parentID=0;
3269          
3270          lstItems.push(albumIdx);
3271          var curIdx=albumIdx;
3272          newLevel++;
3273          
3274          while( G.I[curIdx].albumID != 0 && G.I[curIdx].albumID != -1) {
3275            for(var i=1; i < l; i++ ) {
3276              if( G.I[i].GetID() == G.I[curIdx].albumID ) {
3277                curIdx=i;
3278                lstItems.push(curIdx);
3279                newLevel++;
3280                break;
3281              }
3282            }
3283          }
3284        }
3285        
3286        // build breadcrumb
3287        if( !(G.O.breadcrumbAutoHideTopLevel && newLevel == 0) ) {
3288          BreadcrumbBuild( lstItems );
3289        }
3290      }
3291
3292      
3293      //-- manage and build tag filters
3294      if( G.galleryFilterTags.Get() != false ) {
3295        var nTags = G.I[albumIdx].albumTagList.length;
3296        if( nTags > 0 ) {
3297          for(var i = 0; i < nTags; i++ ) {
3298            var s = G.I[albumIdx].albumTagList[i];
3299            var ic = G.O.icons.navigationFilterUnselected;
3300            var tagClass = 'Unselected';
3301            if( jQuery.inArray(s, G.I[albumIdx].albumTagListSel) >= 0 ) {
3302              tagClass = 'Selected';
3303              ic = G.O.icons.navigationFilterSelected;
3304            }
3305            
3306            var $newTag = jQuery('<div class="nGY2NavigationbarItem nGY2NavFilter' + tagClass + '">'+ ic +' '+ s +'</div>').appendTo(G.GOM.navigationBar.$newContent);
3307
3308            $newTag.click(function() {
3309  
3310              var $this = jQuery(this);
3311              var tag = $this.text().replace(/^\s*|\s*$/, '');  //trim trailing/leading whitespace
3312  
3313              if( G.galleryFilterTagsMode.Get() == 'single' ) {
3314                // single TAG selection
3315                G.I[albumIdx].albumTagListSel = [];
3316                G.I[albumIdx].albumTagListSel.push(tag);
3317              }
3318              else {
3319                // multiple selection of TAGS
3320                // if( $this.hasClass('oneTagUnselected') ){
3321                if( $this.hasClass('nGY2NavFilterUnselected') ){
3322                  G.I[albumIdx].albumTagListSel.push(tag);
3323                }
3324                else {
3325                  var tidx=jQuery.inArray(tag,G.I[albumIdx].albumTagListSel);
3326                  if( tidx != -1 ) {
3327                    G.I[albumIdx].albumTagListSel.splice(tidx,1);
3328                  }
3329                }
3330                $this.toggleClass('nGY2NavFilters-oneTagUnselected nGY2NavFilters-oneTagSelected');
3331              }
3332              
3333              DisplayAlbum('-1', G.I[albumIdx].GetID());
3334            });
3335          }
3336          
3337          // clear/reset TAGS selection
3338          var $newClearFilter=jQuery('<div class="nGY2NavigationbarItem nGY2NavFilterSelectAll">'+ G.O.icons.navigationFilterSelectedAll +'</div>').appendTo(G.GOM.navigationBar.$newContent);
3339          $newClearFilter.click(function() {
3340            // var nTags = G.I[albumIdx].albumTagList.length;
3341            G.I[albumIdx].albumTagListSel = [];
3342            // for(var i = 0; i < nTags; i++ ) {
3343            //  var s = G.I[albumIdx].albumTagList[i];
3344            //  G.I[albumIdx].albumTagListSel.push(s);
3345            // }
3346            DisplayAlbum('-1', G.I[albumIdx].GetID());
3347          });
3348        }
3349      }
3350      
3351      // --- Gallery pagination next/previous
3352      if( G.galleryDisplayMode.Get() == "PAGINATION" && G.O.galleryPaginationTopButtons ) {
3353          if( G.layout.support.rows && G.galleryMaxRows.Get() > 0 ) {
3354            // ManagePagination( G.GOM.albumIdx );
3355            var $newTagPrev = jQuery('<div class="nGY2NavigationbarItem nGY2NavPagination">'+G.O.icons.navigationPaginationPrevious+'</div>').appendTo(G.GOM.navigationBar.$newContent);
3356            $newTagPrev.click(function() {
3357              paginationPreviousPage();
3358            });
3359            var $newTagNext = jQuery('<div class="nGY2NavigationbarItem nGY2NavPagination">'+G.O.icons.navigationPaginationNext+'</div>').appendTo(G.GOM.navigationBar.$newContent);
3360            $newTagNext.click(function() {
3361              paginationNextPage();
3362            });
3363          }
3364      }
3365
3366    }
3367    
3368    function BreadcrumbBuild(lstItems) {
3369
3370      // console.log(G.GOM.navigationBar.$newContent);
3371      jQuery('<div class="nGY2NavigationbarItem nGY2Breadcrumb"></div>').appendTo(G.GOM.navigationBar.$newContent);
3372      // console.log(G.GOM.navigationBar.$newContent);
3373      
3374      if( G.O.breadcrumbOnlyCurrentLevel ) {
3375        // display only 1 separator and the current folder level
3376        if( lstItems.length == 0 ) {
3377          breadcrumbAdd(0);
3378        }
3379        else {
3380          // var last=lstItems.length-1;
3381          if( lstItems.length == 1 ) {
3382            breadcrumbAddSeparator(0);    // root level
3383          }
3384          else {
3385            breadcrumbAddSeparator(lstItems[0]);
3386          }
3387          breadcrumbAdd(lstItems[0]);
3388        }
3389      }
3390      else {
3391        // display the full breadcrum (full folder levels including root level)
3392        breadcrumbAdd(0);
3393        if( lstItems.length > 0 ) {
3394          breadcrumbAddSeparator(0);
3395          for(var i=lstItems.length-1; i>=0 ; i-- ) {
3396            breadcrumbAdd(lstItems[i]);
3397            if( i > 0 ) {
3398              breadcrumbAddSeparator(lstItems[i-1]);
3399            }
3400          }
3401        }
3402      }
3403    
3404    }
3405    
3406
3407    // Display gallery pagination
3408    function ManagePagination() {
3409
3410      G.$E.conTnBottom.css('opacity', 0);
3411      G.$E.conTnBottom.children().remove();
3412
3413      if( G.GOM.items.length == 0 ) { return; }   // no thumbnail to display
3414
3415      // calculate the number of pages
3416      var nbPages = Math.ceil((G.GOM.items[G.GOM.items.length - 1].row + 1)/G.galleryMaxRows.Get());
3417
3418      // only one page -> do not display pagination
3419      if( nbPages == 1 ) { return; }
3420
3421      // check if current page still exists (for example after a resize)
3422      if( G.GOM.pagination.currentPage > (nbPages-1) ) {
3423        G.GOM.pagination.currentPage = nbPages-1;
3424      }
3425      
3426      GalleryRenderGetInterval();
3427      // nothing to display --> exit
3428      if( G.GOM.displayInterval.len == 0 ) { return; }
3429      
3430      // display "previous"
3431      if( G.O.galleryPaginationMode == 'NUMBERS' && G.GOM.pagination.currentPage > 0 ) {
3432        var $eltPrev = jQuery('<div class="nGY2PaginationPrev">'+G.O.icons.paginationPrevious+'</div>').appendTo(G.$E.conTnBottom);
3433        $eltPrev.click(function(e) {
3434          paginationPreviousPage();
3435        });
3436      }
3437
3438      var firstPage = 0;
3439      var lastPage = nbPages;
3440      if( G.O.galleryPaginationMode != 'NUMBERS' ) {
3441        // no 'previous'/'next' and no max number of pagination items
3442        firstPage = 0;
3443      }
3444      else {
3445        // display pagination numbers and previous/next
3446        // var vp = G.O.paginationVisiblePages;
3447        var numberOfPagesToDisplay = G.O.paginationVisiblePages;
3448        if( numberOfPagesToDisplay >= nbPages ) {
3449          firstPage = 0;
3450        }
3451        else {
3452          // we have more pages than we want to display
3453          var nbBeforeAfter = 0;
3454          if( isOdd(numberOfPagesToDisplay) ) {
3455            nbBeforeAfter = (numberOfPagesToDisplay + 1) / 2;
3456          }
3457          else {
3458            nbBeforeAfter = numberOfPagesToDisplay / 2;
3459          }
3460          
3461          if( G.GOM.pagination.currentPage < nbBeforeAfter ) {
3462            firstPage = 0;
3463            lastPage = numberOfPagesToDisplay - 1;
3464            if( lastPage > nbPages ) {
3465              lastPage = nbPages - 1;
3466            }
3467          }
3468          else {
3469            firstPage = G.GOM.pagination.currentPage - nbBeforeAfter;
3470            lastPage = firstPage + numberOfPagesToDisplay;
3471            if( lastPage > nbPages ) {
3472              lastPage = nbPages - 1;
3473            }
3474          }
3475          
3476          if( (lastPage - firstPage) < numberOfPagesToDisplay ) {
3477            firstPage = lastPage - numberOfPagesToDisplay;
3478            if( firstPage < 0 ) {
3479              firstPage = 0;
3480            }
3481          }
3482
3483        }
3484      }
3485
3486      // render pagination items
3487      for(var i = firstPage; i < lastPage; i++ ) {
3488        var c = '';
3489        var p = '';
3490
3491        switch( G.O.galleryPaginationMode ) {
3492          case 'NUMBERS':
3493            c = 'nGY2paginationItem';
3494            p = i + 1;
3495            break;
3496          case 'DOTS':
3497            c = 'nGY2paginationDot';
3498            break;
3499          case 'RECTANGLES':
3500            c = 'nGY2paginationRectangle';
3501            break;
3502        }
3503        if( i == G.GOM.pagination.currentPage ) {
3504          c += 'CurrentPage';
3505        }
3506
3507        var elt$ = jQuery('<div class="' + c + '">' + p + '</div>').appendTo(G.$E.conTnBottom);
3508        elt$.data('pageNumber', i );
3509        elt$.click( function(e) {
3510          G.GOM.pagination.currentPage = jQuery(this).data('pageNumber');
3511          TriggerCustomEvent('pageChanged');
3512
3513          // scroll to top of gallery if not displayed
3514          G.GOM.ScrollToTop();
3515 
3516          GalleryDisplayPart1();
3517          GalleryDisplayPart2( true );
3518        });
3519
3520      }
3521
3522      // display "next"
3523      if( G.O.galleryPaginationMode == 'NUMBERS' && (G.GOM.pagination.currentPage + 1) < nbPages ) {
3524        var $eltNext = jQuery('<div class="nGY2PaginationNext">' + G.O.icons.paginationNext + '</div>').appendTo(G.$E.conTnBottom);
3525        $eltNext.click( function(e) {
3526          paginationNextPage();
3527        });
3528      }
3529
3530      G.$E.conTnBottom.css('opacity', 1);
3531
3532    }
3533    function isOdd(num) { return (num % 2) == 1;}
3534    
3535    // pagination - next page
3536    function paginationNextPage() {
3537      // var aIdx = G.GOM.albumIdx;
3538      var n1 = 0;
3539      ThumbnailHoverOutAll();
3540      
3541      // pagination - max lines per page mode
3542      if( G.galleryMaxRows.Get() > 0 ) {
3543        // number of pages
3544        n1 = (G.GOM.items[G.GOM.items.length - 1].row + 1) / G.galleryMaxRows.Get();
3545      }
3546      var n2 = Math.ceil(n1);
3547      var pn = G.GOM.pagination.currentPage;
3548      if( pn < (n2-1) ) {
3549        pn++;
3550      }
3551      else {
3552        pn = 0;
3553      }
3554      
3555      G.GOM.pagination.currentPage = pn;
3556      TriggerCustomEvent('pageChanged');
3557
3558      // scroll to top of gallery if not displayed
3559      G.GOM.ScrollToTop();
3560
3561      GalleryDisplayPart1();
3562      GalleryDisplayPart2( true );
3563    }
3564    
3565    // pagination - previous page
3566    function paginationPreviousPage() {
3567      // var aIdx=G.$E.conTnBottom.data('galleryIdx'),
3568      // var aIdx = G.GOM.albumIdx;
3569      var n1 = 0;
3570
3571      ThumbnailHoverOutAll();
3572      
3573      // pagination - max lines per page mode
3574      if( G.galleryMaxRows.Get() > 0 ) {
3575        // number of pages
3576        n1 = (G.GOM.items[G.GOM.items.length - 1].row + 1) / G.galleryMaxRows.Get();
3577      }
3578      var n2 = Math.ceil(n1);
3579      
3580      // var pn=G.$E.conTnBottom.data('currentPageNumber');
3581      var pn = G.GOM.pagination.currentPage;
3582      if( pn > 0 ) {
3583        pn--;
3584      }
3585      else {
3586        pn = n2 - 1;
3587      }
3588
3589      G.GOM.pagination.currentPage = pn;
3590      TriggerCustomEvent('pageChanged');
3591
3592      // scroll to top of gallery if not displayed
3593      G.GOM.ScrollToTop();
3594
3595      GalleryDisplayPart1();
3596      GalleryDisplayPart2( true );
3597    }
3598
3599    // retrieve the from/to intervall for gallery thumbnail render
3600    function GalleryRenderGetInterval() {
3601      G.GOM.displayInterval.from = 0;
3602      G.GOM.displayInterval.len = G.I.length;
3603      
3604      switch( G.galleryDisplayMode.Get() ) {
3605        case 'PAGINATION':
3606          if( G.layout.support.rows ) {
3607            let nbTn = G.GOM.items.length;
3608            var firstRow = G.GOM.pagination.currentPage * G.galleryMaxRows.Get();
3609            var lastRow = firstRow + G.galleryMaxRows.Get();
3610            var firstTn = -1;
3611            G.GOM.displayInterval.len = 0;
3612            for( var i = 0; i < nbTn ; i++ ) {
3613              let curTn = G.GOM.items[i];
3614              if( curTn.row >= firstRow && curTn.row < lastRow ) {
3615                if( firstTn == -1 ) {
3616                  G.GOM.displayInterval.from = i;
3617                  firstTn = i;
3618                }
3619                G.GOM.displayInterval.len++;
3620              }
3621            }
3622          }
3623          break;
3624        case 'MOREBUTTON':
3625          if( G.layout.support.rows ) {
3626            let nbTn = G.GOM.items.length;
3627            let lastRow = G.O.galleryDisplayMoreStep * (G.GOM.displayedMoreSteps+1);
3628            G.GOM.displayInterval.len = 0;
3629            for( var i = 0; i < nbTn ; i++ ) {
3630              let curTn = G.GOM.items[i];
3631              if( curTn.row < lastRow ) {
3632                G.GOM.displayInterval.len++;
3633              }
3634            }
3635          }
3636          break;
3637        case 'ROWS':
3638          if( G.layout.support.rows ) {
3639            let nbTn = G.GOM.items.length;
3640            let lastRow = G.galleryMaxRows.Get();
3641            if( G.galleryLastRowFull.Get() && G.GOM.lastFullRow != -1 ) {
3642              if( lastRow > (G.GOM.lastFullRow + 1) ) {
3643                lastRow = G.GOM.lastFullRow + 1;
3644              }
3645            }
3646            G.GOM.displayInterval.len = 0;
3647            for( var i = 0; i < nbTn ; i++ ) {
3648              let curTn = G.GOM.items[i];
3649              if( curTn.row < lastRow ) {
3650                G.GOM.displayInterval.len++;
3651              }
3652            }
3653          }
3654          break;
3655        default:
3656        case 'FULLCONTENT':
3657        if( G.layout.support.rows && G.galleryLastRowFull.Get() && G.GOM.lastFullRow != -1 ) {
3658            let nbTn = G.GOM.items.length;
3659            let lastRow = G.GOM.lastFullRow + 1;
3660            G.GOM.displayInterval.len = 0;
3661            for( var i = 0; i < nbTn ; i++ ) {
3662              let curTn = G.GOM.items[i];
3663              if( curTn.row < lastRow ) {
3664                G.GOM.displayInterval.len++;
3665              }
3666            }
3667          }
3668          break;
3669      }
3670    }
3671    
3672    
3673    // RENDER THE GALLERY
3674    function GalleryRender( albumIdx ) {
3675      TriggerCustomEvent('galleryRenderStart');
3676      clearTimeout(G.GOM.slider.timerID);
3677      G.GOM.slider.hostIdx = -1;      // disabled slider on thumbnail
3678      
3679      var fu=G.O.fnGalleryRenderStart;
3680      if( fu !== null ) {
3681        // typeof fu == 'function' ? fu(albumIdx) : window[fu](albumIdx);
3682        typeof fu == 'function' ? fu( G.I[G.GOM.albumIdx] ) : window[fu]( G.I[G.GOM.albumIdx] );
3683      }
3684
3685      G.layout.SetEngine();
3686      G.galleryResizeEventEnabled = false;
3687      G.GOM.albumIdx = -1;
3688      G.GOM.lastDisplayedIdx = -1;
3689
3690      // pagination
3691      if( G.$E.conTnBottom !== undefined ) {
3692        // G.$E.conTnBottom.children().remove();
3693        G.$E.conTnBottom.empty();
3694      }
3695
3696      // navigation toolbar (breadcrumb + tag filters + pagination next/previous)
3697      GalleryNavigationBar(albumIdx);
3698      
3699      if( G.GOM.firstDisplay ) {
3700				// first gallery display
3701        G.GOM.firstDisplay = false;
3702        var d = Date.now()-G.GOM.firstDisplayTime;
3703        if( d < G.O.galleryRenderDelay ) {
3704					// display after defined delay
3705          // setTimeout( function() { GalleryRenderPart1( albumIdx )}, G.O.galleryRenderDelay-d );
3706          requestTimeout( function() { GalleryRenderPart1( albumIdx )}, G.O.galleryRenderDelay-d );
3707        }
3708        else {
3709          GalleryRenderPart1( albumIdx );
3710        }
3711        G.O.galleryRenderDelay = 0;
3712        
3713      }
3714      else {
3715        var hideNavigationBar = false;
3716        if( G.GOM.navigationBar.$newContent.children().length == 0 ) {
3717          hideNavigationBar = true;
3718        }
3719
3720        // hide everything
3721        var tweenable = new NGTweenable();
3722        tweenable.tween({
3723          from:         { 'opacity': 1 },
3724          to:           { 'opacity': 0 },
3725          duration:     300,
3726          easing:       'easeInQuart',
3727          attachment:   { h: hideNavigationBar },
3728          step:         function (state, att) {
3729            G.$E.conTnParent.css({'opacity': state.opacity });
3730            if( att.h ) {
3731              G.$E.conNavigationBar.css({ 'opacity': state.opacity });
3732            }
3733          },
3734          finish:       function (state, att) {
3735            if( att.h ) {
3736              G.$E.conNavigationBar.css({ 'opacity': 0, 'display': 'none' });
3737            }
3738            // scroll to top of the gallery if needed
3739
3740            G.GOM.ScrollToTop();
3741
3742            GalleryRenderPart1( albumIdx );
3743          }
3744        });
3745      }
3746    }
3747
3748
3749    function GalleryRenderPart1( albumIdx ) {
3750      // display new navigation bar
3751      var oldN = G.$E.conNavigationBar.children().length;
3752      G.$E.conNavigationBar.empty();
3753      G.GOM.navigationBar.$newContent.children().clone(true,true).appendTo(G.$E.conNavigationBar);
3754      // G.GOM.navigationBar.$newContent.appendTo(G.$E.conNavigationBar);
3755      if( G.$E.conNavigationBar.children().length > 0 && oldN == 0 ) {
3756        G.$E.conNavigationBar.css({ 'opacity': 0, 'display': 'block' });
3757        var tweenable = new NGTweenable();
3758        tweenable.tween({
3759          from:     { opacity: 0 },
3760          to:       { opacity: 1 },
3761          duration: 200,
3762          easing:   'easeInQuart',
3763          step:     function (state) {
3764            // window.ng_draf( function() {
3765              G.$E.conNavigationBar.css( state );
3766            // });
3767          },
3768          finish:   function (state) {
3769            // window.ng_draf( function() {
3770              G.$E.conNavigationBar.css({ 'opacity': 1 });
3771              // display gallery
3772              // GalleryRenderPart2( albumIdx );
3773              // setTimeout(function(){ GalleryRenderPart2(albumIdx) }, 60);
3774              requestTimeout(function(){ GalleryRenderPart2(albumIdx) }, 20);
3775            // });
3776          }
3777        });
3778      }
3779      else {
3780        requestTimeout(function(){ GalleryRenderPart2(albumIdx) }, 20);
3781      }
3782
3783    }
3784    
3785    // Gallery render part 2 -> remove all existing thumbnails
3786    function GalleryRenderPart2(albumIdx) {
3787
3788			G.GOM.lastZIndex = parseInt(G.$E.base.css('z-index'));
3789      if( isNaN(G.GOM.lastZIndex) ) {
3790        G.GOM.lastZIndex=0;
3791      }
3792      G.$E.conTnParent.css({ 'opacity': 0 });
3793      G.$E.conTn.off().empty();
3794      var l = G.I.length;
3795      for( var i = 0; i < l ; i++ ) {
3796        // reset each item
3797        var item = G.I[i];
3798        item.hovered = false;
3799        item.$elt = null;
3800        item.$Elts = [];
3801        item.eltTransform = [];
3802        item.eltFilter = [];
3803        item.width = 0;
3804        item.height = 0;
3805        item.left = 0;
3806        item.top = 0;
3807        item.resizedContentWidth = 0;
3808        item.resizedContentHeight = 0;
3809        item.thumbnailImgRevealed = false;
3810      }
3811
3812      if( G.CSStransformName == null ) {
3813        G.$E.conTn.css('left', '0px');
3814      }
3815      else {
3816        // G.$E.conTn.css( G.CSStransformName, 'translateX(0px)');
3817        G.$E.conTn.css( G.CSStransformName, 'none');
3818      }
3819      
3820      // setTimeout(function(){ GalleryRenderPart3(albumIdx) }, 60);
3821      requestTimeout(function(){ GalleryRenderPart3(albumIdx) }, 20);
3822      // GalleryRenderPart3(albumIdx);
3823
3824    }
3825    
3826    // Gallery render part 3 -> start building the new gallery
3827    function GalleryRenderPart3(albumIdx) {
3828      var d = new Date();      
3829      
3830      G.$E.conTnParent.css( 'opacity', 1);
3831
3832      G.GOM.items = [];
3833      G.GOM.displayedMoreSteps = 0;
3834      // retrieve label height      
3835      if( G.O.thumbnailLabel.get('position') == 'onBottom' ) {
3836        // retrieve height each time because size can change depending on thumbnail's settings
3837        G.tn.labelHeight[G.GOM.curNavLevel] = ThumbnailGetLabelHeight();
3838      }
3839      else {
3840        G.tn.labelHeight[G.GOM.curNavLevel] = 0;
3841      }
3842      G.GOM.albumIdx=albumIdx;
3843
3844      TriggerCustomEvent('galleryRenderEnd');
3845      var fu=G.O.fnGalleryRenderEnd;
3846      if( fu !== null ) {
3847        // typeof fu == 'function' ? fu(albumIdx) : window[fu](albumIdx);
3848        typeof fu == 'function' ? fu(G.I[G.GOM.albumIdx] ) : window[fu](G.I[G.GOM.albumIdx] );
3849      }
3850
3851      // Step 1: populate GOM
3852      if( GalleryPopulateGOM() ) {
3853        // step 2: calculate layout
3854        GallerySetLayout();
3855
3856        // step 3: display whole gallery 
3857        GalleryAppear();
3858        
3859        // step 4: display thumbnails
3860        GalleryDisplayPart1();
3861        requestTimeout(function(){ GalleryDisplayPart2( false ) }, 20);
3862      }
3863      else {
3864        // 
3865        G.galleryResizeEventEnabled = true;
3866      }
3867      
3868      if( G.O.debugMode ) { console.log('GalleryRenderPart3: '+ (new Date()-d)); }
3869
3870    }
3871    
3872    
3873    // Resize the gallery
3874    function GalleryResize() {
3875      var d = new Date();
3876      G.galleryResizeEventEnabled = false;
3877      // G.GOM.cache.areaWidth=G.$E.conTnParent.width();
3878      if( GallerySetLayout() == false ) {
3879        G.galleryResizeEventEnabled = true;
3880        if( G.O.debugMode ) { console.log('GalleryResize1: '+ (new Date()-d)); }
3881        return;
3882      }
3883      if( G.O.debugMode ) { console.log('GalleryResizeSetLayout: '+ (new Date()-d)); }
3884
3885      GalleryDisplayPart1();
3886      GalleryDisplayPart2( false );
3887
3888      if( G.O.debugMode ) { console.log('GalleryResizeFull: '+ (new Date()-d)); }
3889    }
3890    
3891    
3892    
3893    // copy items (album content) to GOM
3894		// returns:
3895		//		true: thumbnail image size is needed for the layout, but not set -> retrieve the sizes and display gallery
3896    function GalleryPopulateGOM() {
3897      
3898      var preloadImages = '';
3899      var albumID = G.I[G.GOM.albumIdx].GetID();
3900      var l = G.I.length;
3901      var cnt = 0;
3902
3903      for( var idx = 0; idx < l; idx++ ) {
3904        var item = G.I[idx];
3905        // check album
3906        if( item.isToDisplay(albumID) ) {
3907          var w = item.thumbImg().width;
3908          var h = item.thumbImg().height;
3909          // if unknown image size and layout is not grid --> we need to retrieve the size of the images
3910          if( G.layout.prerequisite.imageSize && ( w == 0 || h == 0) ) {
3911            preloadImages += '<img src="'+item.thumbImg().src+'" data-idx="'+cnt+'" data-albumidx="'+G.GOM.albumIdx+'">';
3912          }
3913          
3914          // set default size if required
3915          if( h == 0 ) {
3916            h = G.tn.defaultSize.getHeight();
3917          }
3918          if( w == 0 ) {
3919            w = G.tn.defaultSize.getWidth();
3920          }
3921          var tn = new G.GOM.GTn(idx, w, h);
3922          G.GOM.items.push(tn);
3923          cnt++;
3924        }
3925      }
3926
3927      TriggerCustomEvent('galleryObjectModelBuilt');
3928      var fu = G.O.fnGalleryObjectModelBuilt;
3929      if( fu !== null ) {
3930        typeof fu == 'function' ? fu() : window[fu]();
3931      }
3932
3933      if( preloadImages != '' ) {
3934        // preload images to retrieve their size and then resize the gallery (=GallerySetLayout()+ GalleryDisplay())
3935        var $newImg = jQuery(preloadImages);
3936        var gi_imgLoad = ngimagesLoaded( $newImg );
3937        $newImg = null;
3938        gi_imgLoad.on( 'progress', function( instance, image ) {
3939        
3940          if( image.isLoaded ) {
3941            var idx = image.img.getAttribute('data-idx');
3942            var albumIdx = image.img.getAttribute('data-albumidx');
3943            if( albumIdx == G.GOM.albumIdx ) {
3944              // ignore event if not on current album
3945              var curTn = G.GOM.items[idx];
3946              curTn.imageWidth = image.img.naturalWidth;
3947              curTn.imageHeight = image.img.naturalHeight;
3948              var item = G.I[curTn.thumbnailIdx];
3949              item.thumbs.width[G.GOM.curNavLevel][G.GOM.curWidth] = curTn.imageWidth;
3950              item.thumbs.height[G.GOM.curNavLevel][G.GOM.curWidth] = curTn.imageHeight;
3951 
3952              // resize the gallery
3953              G.GalleryResizeThrottled();
3954              
3955              // set the retrieved size to all levels with same configuration  
3956              var object = item.thumbs.width.l1;
3957              for (let property in object) {
3958                if (object.hasOwnProperty(property)) {
3959                  if( property != G.GOM.curWidth ) {
3960                    if( G.tn.settings.width.l1[property] == G.tn.settings.getW() && G.tn.settings.height.l1[property] == G.tn.settings.getH() ) {
3961                      item.thumbs.width.l1[property] = curTn.imageWidth;
3962                      item.thumbs.height.l1[property] = curTn.imageHeight;
3963                    }
3964                  }
3965                }
3966              }
3967              object = item.thumbs.width.lN;
3968              for (let property in object) {
3969                if (object.hasOwnProperty(property)) {
3970                  if( property != G.GOM.curWidth ) {
3971                    if( G.tn.settings.width.lN[property] == G.tn.settings.getW() && G.tn.settings.height.lN[property] == G.tn.settings.getH() ) {
3972                      item.thumbs.width.lN[property] = curTn.imageWidth;
3973                      item.thumbs.height.lN[property] = curTn.imageHeight;
3974                    }
3975                  }
3976                }
3977              }
3978            }
3979          }
3980        });
3981        G.galleryResizeEventEnabled = true;
3982        return false;
3983      }
3984      else {
3985        return true;
3986      }
3987      
3988    }
3989    
3990    //----- Calculate the layout of the thumbnails for the full gallery
3991    function GallerySetLayout() {
3992      var r = true;
3993      // width of the available area
3994      G.GOM.cache.areaWidth = G.$E.conTnParent.width();
3995      G.GOM.displayArea = { width:0, height:0 };
3996
3997      switch( G.layout.engine ) {
3998        case 'JUSTIFIED':
3999          r = GallerySetLayoutWidthtAuto();
4000          break;
4001        case 'CASCADING':
4002          r = GallerySetLayoutHeightAuto();
4003          break;
4004        case 'MOSAIC':
4005          r = GallerySetLayoutMosaic();
4006          break;
4007        case 'GRID':
4008        default:
4009          r = GallerySetLayoutGrid();
4010          break;
4011      }
4012      
4013      TriggerCustomEvent('galleryLayoutApplied');
4014      var fu = G.O.fnGalleryLayoutApplied;
4015      if( fu !== null ) {
4016        typeof fu == 'function' ? fu() : window[fu]();
4017      }
4018      return r;
4019
4020    }
4021    
4022    
4023    //----- CASCADING LAYOUT
4024    function GallerySetLayoutHeightAuto() {
4025      var curCol =      0,
4026      areaWidth =       G.GOM.cache.areaWidth,
4027      curRow =          0,
4028      colHeight =       [],
4029      maxCol =          NbThumbnailsPerRow(areaWidth),
4030      gutterWidth =     0,
4031      gutterHeight =    G.tn.settings.GetResponsive('gutterHeight');
4032      var w =           0;
4033      var scaleFactor = 1;
4034      var tnWidth =     G.tn.defaultSize.getOuterWidth();
4035      var nbTn =        G.GOM.items.length;
4036      var curPosY = 0;
4037
4038      if( G.O.thumbnailAlignment == 'justified' ) {
4039        maxCol = Math.min(maxCol, nbTn);
4040        gutterWidth = ( maxCol == 1 ? 0 : (areaWidth - (maxCol * tnWidth) ) / (maxCol - 1) );
4041      }
4042      else {
4043        gutterWidth = G.tn.settings.GetResponsive('gutterWidth');
4044      }
4045
4046
4047      var borderWidth = G.tn.opt.Get('borderHorizontal') * 2;
4048      var borderHeight = G.tn.opt.Get('borderVertical') * 2;
4049
4050      G.GOM.lastFullRow=-1;   // feature disabled
4051
4052      // Retrieve the real used width of the area (the evaluation is based on the content of the first line)
4053      if( G.O.thumbnailAlignment == 'fillWidth' ) {
4054        // fillWidth --> evaluate scale factor and number of columns
4055        var totalGutterWidth = (maxCol - 1) * gutterWidth;
4056        scaleFactor = (areaWidth - totalGutterWidth) / (maxCol * tnWidth);
4057        if( scaleFactor > 1 ) {
4058          maxCol++; // add one column and re-evaluate the scale factor
4059        }
4060        totalGutterWidth = (maxCol - 1) * gutterWidth;
4061        scaleFactor = Math.min( (areaWidth - totalGutterWidth) / (maxCol*tnWidth), 1);   // no upscale
4062      }
4063
4064      
4065      tnWidth = Math.round( tnWidth * scaleFactor);
4066      var contentWidth = tnWidth - borderWidth;
4067
4068      // loop to position the thumbnails, and set their size
4069      var baseHeight = Math.round( G.tn.opt.Get('baseGridHeight') * scaleFactor );
4070      for( var i = 0; i < nbTn ; i++ ) {
4071        var curTn = G.GOM.items[i];
4072        if( curTn.deleted == true ) { break; }    // item is logically deleted
4073        if( curTn.imageHeight > 0 && curTn.imageWidth > 0 ) {
4074          var curPosX = 0,
4075          curPosY = 0;
4076          var imageRatio = curTn.imageHeight / curTn.imageWidth;
4077          // curTn.resizedContentWidth = tnWidth - borderWidth;
4078          curTn.resizedContentWidth = contentWidth;
4079          curTn.resizedContentHeight = curTn.resizedContentWidth * imageRatio;
4080          if( baseHeight > 0 ) {
4081            // grid based vertical position
4082            var t = Math.max( Math.trunc(curTn.resizedContentHeight/baseHeight), 1) ;
4083            curTn.resizedContentHeight = baseHeight * t + ((t-1)*(borderHeight+gutterHeight));
4084          }
4085          
4086          curTn.height = curTn.resizedContentHeight + borderHeight + G.tn.labelHeight.get();
4087          curTn.width = tnWidth;
4088          curTn.row = 0;
4089          
4090          if( curRow == 0 ) {
4091            // first row
4092            curPosX = curCol * (tnWidth + gutterWidth);
4093            colHeight[curCol] = curTn.height + gutterHeight;
4094            
4095            curCol++;
4096            if( curCol >= maxCol ) {
4097              curCol = 0;
4098              curRow++;
4099            }
4100          }
4101          else {
4102            var c=0,
4103            minColHeight=colHeight[0];
4104            for( var j = 1; j < maxCol; j++) {
4105              if( (colHeight[j] + 5) < minColHeight ) {     // +5 --> threshold
4106                minColHeight = colHeight[j];
4107                c = j;
4108                //break;
4109              }
4110            }
4111            curPosY = colHeight[c];
4112            curPosX = c * (tnWidth + gutterWidth);
4113            colHeight[c] = curPosY + curTn.height + gutterHeight;
4114          }
4115
4116          var x = curPosX;
4117          if( G.O.RTL) {
4118            x= w - curPosX - tnWidth;
4119          }
4120
4121          curTn.left = x;
4122          curTn.top = curPosY;
4123        }
4124      }
4125
4126      G.GOM.displayArea.width= maxCol * (tnWidth + gutterWidth) - gutterWidth;
4127      return true;
4128    }
4129    
4130    
4131    //----- JUSTIFIED LAYOUT
4132    function GallerySetLayoutWidthtAuto() {
4133      var curWidth =               0,
4134      areaWidth =                  G.GOM.cache.areaWidth,
4135      lastPosX =                   0,
4136      curPosY =                    0,
4137      rowLastItem =                [],
4138      rowNum =                     0,
4139      rowHeight =                  [],
4140      bNewRow =                    false,
4141      cnt =                        0,
4142      gutterWidth =                G.tn.settings.GetResponsive('gutterWidth'),
4143      gutterHeight =               G.tn.settings.GetResponsive('gutterHeight');
4144      // by grief-of-these-days
4145      var maxRowHeightVertical =   0; // max height of a row with vertical thumbs
4146      var maxRowHeightHorizontal = 0; // max height of a row with horizontal thumbs
4147      var rowHasVertical =         false; // current row has vertical thumbs
4148      var rowHasHorizontal =       false; // current row has horizontal thumbs
4149
4150      var tnHeight = G.tn.defaultSize.getOuterHeight();
4151      var borderWidth = G.tn.opt.Get('borderHorizontal') * 2;
4152      var borderHeight = G.tn.opt.Get('borderVertical') * 2;
4153      var nbTnInCurrRow = 1;
4154      var nbTn = G.GOM.items.length;
4155
4156      // first loop --> retrieve each row image height
4157      for( var i = 0; i < nbTn ; i++ ) {
4158        let curTn = G.GOM.items[i];
4159        if( curTn.deleted == true ) { break; }    // item is logically deleted
4160        if( curTn.imageWidth > 0 ) {
4161          let imageRatio = curTn.imageWidth / curTn.imageHeight;
4162          let imageWidth = Math.floor( tnHeight * imageRatio );
4163
4164          if( bNewRow ) {
4165            bNewRow = false;
4166            rowNum++;
4167            curWidth = 0;
4168            rowHasVertical = false;
4169            rowHasHorizontal = false;
4170            nbTnInCurrRow = 1;
4171          }
4172          // by grief-of-these-days
4173          if( curTn.imageHeight > curTn.imageWidth ) {
4174            rowHasVertical = true;
4175          }
4176          else {
4177            rowHasHorizontal = true;
4178          }
4179          
4180          if( (curWidth + gutterWidth + imageWidth) < (areaWidth - (nbTnInCurrRow * borderWidth)) ) {
4181            // enough place left in the current row
4182            curWidth += imageWidth + gutterWidth;
4183            rowHeight[rowNum] = tnHeight;
4184            
4185            // prevent incomplete row from being heigher than the previous ones.
4186            // by grief-of-these-days
4187            var rowHeightLimit = Math.max(rowHasVertical ? maxRowHeightVertical : 0, rowHasHorizontal ? maxRowHeightHorizontal : 0);
4188            if( rowHeightLimit > 0 ) {
4189              rowHeight[rowNum] = Math.min(rowHeight[rowNum], rowHeightLimit);
4190            }
4191            
4192            rowLastItem[rowNum] = i;
4193          }
4194          else {
4195            // new row after current item --> we need to adujet the row height to have enough space for the current thumbnail
4196            curWidth += gutterWidth+imageWidth;
4197            let ratio = (areaWidth - nbTnInCurrRow * borderWidth) / curWidth;
4198            let rH = Math.floor(tnHeight * ratio);
4199            rowHeight[rowNum] = rH;
4200            
4201            // save the max row height for each thumb orientation.
4202            // by grief-of-these-days
4203            if( rowHasVertical ) {
4204              maxRowHeightVertical = Math.max( maxRowHeightVertical, rH );
4205            }
4206            if( rowHasHorizontal ) {
4207              maxRowHeightHorizontal = Math.max( maxRowHeightHorizontal, rH );
4208            }
4209            
4210            rowLastItem[rowNum] = i;
4211            bNewRow = true;
4212          }
4213          cnt++;
4214          nbTnInCurrRow++;
4215        }
4216      }
4217
4218      rowNum = 0;
4219      curPosY = 0;
4220      lastPosX = 0;
4221      cnt = 0;
4222      
4223      G.GOM.lastFullRow = 0;    // display at leat 1 row (even if not full)
4224      
4225      // second loop --> calculate each thumbnail size
4226      for( var i = 0; i < nbTn ; i++ ) {
4227        let curTn = G.GOM.items[i];
4228        if( curTn.imageWidth > 0 ) {
4229          let imageRatio = curTn.imageWidth / curTn.imageHeight;
4230          let imageWidth = Math.floor( imageRatio * rowHeight[rowNum] ); // border is already NOT included
4231
4232          if( i == rowLastItem[rowNum] ) {
4233            // row last item --> adjust image width because of rounding problems
4234            if( rowLastItem.length != (rowNum+1) ) {
4235              // last item in current row -> use the full remaining width
4236              imageWidth = areaWidth - lastPosX - borderWidth;
4237            }
4238            else {
4239              // very last item (on the last row)
4240              if( (lastPosX + gutterWidth + imageWidth + borderWidth ) > areaWidth ) {
4241                // reduce size if image is wider as the remaining space
4242                imageWidth = areaWidth - lastPosX - borderWidth;
4243              }
4244            }
4245          }
4246          
4247          let rh = parseInt( rowHeight[rowNum] );
4248          imageWidth = parseInt( imageWidth );
4249
4250          // thumbnail image size
4251          curTn.resizedContentWidth = imageWidth;
4252          curTn.resizedContentHeight = rh;
4253          // thumbnail position and size
4254          curTn.width = imageWidth + borderWidth;
4255          curTn.height= rh + G.tn.labelHeight.get() + borderHeight;
4256          curTn.row = rowNum;
4257
4258          curTn.top = curPosY;
4259          let x = lastPosX;
4260          if( G.O.RTL) {
4261            x = areaWidth - lastPosX - curTn.width ;
4262          }
4263          curTn.left = x;
4264
4265          lastPosX += curTn.width + gutterWidth;
4266
4267          if( i == rowLastItem[rowNum] ) {
4268            // start a new row
4269            curPosY += curTn.height + gutterHeight;
4270            G.GOM.lastFullRow = rowNum - 1;
4271            rowNum++;
4272            lastPosX = 0;
4273          }
4274          cnt++;
4275        }
4276        else {
4277          return false;
4278        }
4279      }
4280      
4281      // hover effect on gallery (vs on thumbnail) --> experimental / not used
4282      if( false ) {
4283        var newTop = 0;
4284        if( typeof GOMidx !== 'undefined' ) {
4285          if( G.GOM.albumIdx != -1 ) {
4286            var hoveredTn = G.GOM.items[GOMidx];
4287            
4288            // hovered thumbnail
4289            hoveredTn.width += 40;
4290            hoveredTn.height += 40;
4291            
4292            for( var i = 0; i < nbTn ; i++ ) {
4293              var curTn = G.GOM.items[i];
4294              if( curTn.imageWidth > 0 ) {
4295                if( curTn.row == hoveredTn.row ) {
4296                  // hovered row
4297                  newTop = 40;
4298                  if( hoveredTn.thumbnailIdx != curTn.thumbnailIdx ) {
4299                    // not hovered thumbnail
4300                    curTn.top += 30;
4301                    curTn.width -= 20;
4302                    curTn.height -= 20;
4303                  }
4304                }
4305                else {
4306                  // not hovered row
4307                  if( curTn.row == 0 ) {
4308                    // first row
4309                  }
4310                  else {
4311                    curTn.top += newTop;
4312                  }
4313                }
4314              }
4315            }
4316          }
4317        }
4318      }
4319      
4320      G.GOM.displayArea.width = areaWidth;
4321      return true;
4322    }    
4323    
4324
4325    //----- MOSAIC LAYOUT
4326    // Grid using a user defined pattern layout
4327    // With this layout, a pattern definition is handeld a row
4328    function GallerySetLayoutMosaic() {
4329      var areaWidth =     G.GOM.cache.areaWidth;
4330      var gutterHeight =  G.tn.settings.GetResponsive('gutterHeight');
4331      var gutterWidth =   G.tn.settings.GetResponsive('gutterWidth');
4332      var borderWidth =   G.tn.opt.Get('borderHorizontal') * 2;
4333      var borderHeight =  G.tn.opt.Get('borderVertical') * 2;
4334
4335      var nbTn = G.GOM.items.length;
4336      var row = 0;
4337      var h = 0;
4338      var n = 0;
4339      
4340      
4341      // first loop: evaluate the gallery width based on the first row
4342      var nbCols = 0;
4343      var maxW = 0;
4344      let mosaicPattern = G.tn.settings.getMosaic();
4345      for( var i = 0; i < nbTn ; i++ ) {
4346        let curPatternElt = mosaicPattern[n];
4347
4348        var cLeft = (curPatternElt.c - 1) * G.tn.defaultSize.getOuterWidth() + (curPatternElt.c - 1) * gutterWidth;
4349        var cWidth = curPatternElt.w * G.tn.defaultSize.getOuterWidth() + (curPatternElt.w - 1) * gutterWidth;
4350        
4351        maxW = Math.max(maxW, cLeft + cWidth );
4352        
4353        nbCols = Math.max(nbCols, (curPatternElt.c - 1) + curPatternElt.w );
4354
4355        n++;
4356        if( n >= mosaicPattern.length ) {
4357          // end of pattern
4358          break;
4359        }
4360      }
4361      var totalGutterWidth = (nbCols - 1) * gutterWidth;
4362      var scaleFactor = Math.min( (areaWidth - totalGutterWidth ) / ( maxW - totalGutterWidth ), 1);
4363      
4364      // second loop: position all the thumbnails based on the layout pattern
4365      row = 0;
4366      n = 0;
4367      // let mosaicPattern = G.tn.settings.getMosaic();
4368      for( var i = 0; i < nbTn ; i++ ) {
4369        let curTn = G.GOM.items[i];
4370        let curPatternElt = mosaicPattern[n];
4371        
4372        curTn.top = Math.round((curPatternElt.r - 1) * G.tn.defaultSize.getOuterHeight()*scaleFactor) + (curPatternElt.r - 1) * gutterHeight + row * h + (G.tn.labelHeight.get()*(curPatternElt.r-1)) ;
4373        if( row > 0 ) {
4374          curTn.top += gutterHeight;
4375        }
4376
4377        curTn.left = (curPatternElt.c - 1) * Math.round(G.tn.defaultSize.getOuterWidth()*scaleFactor) + (curPatternElt.c - 1) * gutterWidth;
4378
4379        curTn.height = Math.round(curPatternElt.h * G.tn.defaultSize.getOuterHeight() * scaleFactor) + (curPatternElt.h - 1) * gutterHeight + (G.tn.labelHeight.get() * curPatternElt.h);
4380        curTn.resizedContentHeight = curTn.height - G.tn.labelHeight.get() - borderHeight;
4381
4382        curTn.width = Math.round(curPatternElt.w * G.tn.defaultSize.getOuterWidth()*scaleFactor) + (curPatternElt.w - 1) * gutterWidth;
4383        curTn.resizedContentWidth = curTn.width - borderWidth ;
4384
4385        curTn.row = row;
4386        if( row == 0 ) {
4387          h=Math.max(h, curTn.top + curTn.height);
4388        }
4389
4390        n++;
4391        if( n >= mosaicPattern.length ) {
4392          // end pattern -> new line
4393          n = 0;
4394          row++;
4395        }
4396      }
4397      
4398      G.GOM.displayArea.width = (maxW - totalGutterWidth) * scaleFactor + totalGutterWidth;
4399      return true;
4400    }
4401    
4402    
4403    
4404    // --- GRID LAYOUT
4405    function GallerySetLayoutGrid() {
4406      var curPosX=      0,
4407      curPosY=          0,   
4408      areaWidth=        G.GOM.cache.areaWidth,
4409      gutterWidth=      0,
4410      gutterHeight=     G.tn.settings.GetResponsive('gutterHeight'),
4411      maxCol=           NbThumbnailsPerRow(areaWidth),
4412      w=                0,
4413      cols=             [],
4414      curCol=           0,
4415      newAreaWidth =    areaWidth,
4416      tnWidth=          G.tn.defaultSize.getOuterWidth();
4417      var scaleFactor = 1;
4418      var nbTn=         G.GOM.items.length;
4419      var borderWidth   = G.tn.opt.Get('borderHorizontal') * 2;
4420      var borderHeight  = G.tn.opt.Get('borderVertical') * 2;
4421      
4422      // retrieve gutter width
4423      if( G.O.thumbnailAlignment == 'justified' ) {
4424        maxCol = Math.min( maxCol, nbTn);
4425        gutterWidth = (maxCol==1 ? 0 : (areaWidth-(maxCol*tnWidth))/(maxCol-1));
4426      }
4427      else {
4428        gutterWidth = G.tn.settings.GetResponsive('gutterWidth');
4429      }
4430
4431      // first loop to retrieve the real used width of the area (the evaluation is based on the content of the first line)
4432      // Retrieve the real used width of the area (the evaluation is based on the content of the first line)
4433      if( G.O.RTL || G.O.thumbnailAlignment == 'fillWidth' ) {
4434        // scaled --> evaluate scale factor and number of columns
4435        var totalGutterWidth = (maxCol-1) * gutterWidth;
4436        scaleFactor = (areaWidth - totalGutterWidth) / (maxCol*tnWidth);
4437        if( scaleFactor > 1 ) {
4438          maxCol++; // add one column and re-evaluate the scale factor
4439        }
4440        totalGutterWidth = (maxCol-1) * gutterWidth;
4441        scaleFactor = Math.min( (areaWidth - totalGutterWidth) / (maxCol*tnWidth), 1);   // no upscale
4442        newAreaWidth = (maxCol*tnWidth) + totalGutterWidth;
4443      }
4444
4445      
4446      G.GOM.lastFullRow = 0 ;    // display at leat 1 row (even if not full)
4447      // var lastPosY = 0;
4448      var row = 0;
4449      
4450      tnWidth = Math.round(tnWidth * scaleFactor);
4451      var contentWidth = tnWidth - borderWidth;
4452      var tnHeight = Math.round(G.tn.defaultSize.getOuterHeight() * scaleFactor) + G.tn.labelHeight.get();
4453      var contentHeight = Math.round( G.tn.defaultSize.getOuterHeight() * scaleFactor) - borderHeight;
4454      
4455      // loop to position and to set size of all thumbnails
4456      for( var i = 0; i < nbTn ; i++ ) {
4457        if( curPosY == 0 ) {
4458          curPosX = curCol * (tnWidth + gutterWidth)
4459          cols[curCol] = curPosX;
4460          w = curPosX + tnWidth;
4461        }
4462        else {
4463          curPosX = cols[curCol];
4464        }
4465
4466        var x = curPosX;
4467        if( G.O.RTL ) {
4468          x = parseInt(newAreaWidth) - curPosX - tnWidth;
4469        }
4470        
4471        // MANDATORY : set thumbnail position AND size
4472        var curTn=G.GOM.items[i];
4473        curTn.top = curPosY;
4474        curTn.left = x;
4475        curTn.height = tnHeight;
4476        curTn.width = tnWidth;
4477        // image size
4478        if( G.O.thumbnailAlignment == 'fillWidth' ) {
4479          curTn.resizedContentWidth = contentWidth;
4480          curTn.resizedContentHeight = contentHeight;
4481        }
4482        curTn.row = row;
4483        // lastPosY = curPosY;
4484
4485        curCol++;
4486        if( curCol >= maxCol ){
4487          // new line
4488          curCol = 0;
4489          curPosY += tnHeight + gutterHeight;
4490          G.GOM.lastFullRow = row;
4491          row++;
4492        }
4493      }
4494      G.GOM.displayArea.width = w;
4495
4496      return true;
4497    }
4498
4499
4500
4501
4502    //----- Display the thumbnails according to the calculated layout
4503    function GalleryDisplayPart1() {
4504      if( G.CSStransformName == null ) {
4505        G.$E.conTn.css( 'left' , '0px');
4506      }
4507      else {
4508        G.$E.conTn.css( G.CSStransformName , 'none');
4509      }
4510      // CacheViewport();
4511    }
4512    
4513    function CacheViewport() {
4514      G.GOM.cache.viewport = getViewport();
4515      // G.GOM.cache.areaWidth = G.$E.conTnParent.width();
4516      G.GOM.cache.areaWidth = G.$E.base.width();
4517			
4518			// position of the gallery container
4519			// we use the position of the loadingbar because :
4520			//    - the gallery may be wrong positioned due to one display animation currently running
4521			//    - the loadingbar is never animated and positioned just before the gallery container
4522      //G.GOM.cache.containerOffset = G.$E.conTnParent.offset();
4523      if( !G.O.lightboxStandalone ) {
4524        G.GOM.cache.containerOffset = G.$E.conLoadingB.offset();
4525      }
4526    }
4527
4528
4529    
4530    function GalleryDisplayPart2( forceTransition ) {
4531      CacheViewport();
4532
4533      var nbTn = G.GOM.items.length;
4534      G.GOM.itemsDisplayed = 0;
4535      var threshold = 50;
4536      var cnt = 0;    // counter for delay between each thumbnail display
4537      
4538
4539      GalleryRenderGetInterval();
4540      
4541      for( var i = 0; i < nbTn ; i++ ) {
4542        let curTn = G.GOM.items[i];
4543        if( i >= G.GOM.displayInterval.from && cnt < G.GOM.displayInterval.len ) {
4544          curTn.inDisplayArea = true;
4545          if( forceTransition ) {
4546            curTn.neverDisplayed = true;
4547          }
4548          G.GOM.itemsDisplayed++;
4549          cnt++;
4550        }
4551        else{
4552          curTn.inDisplayArea = false;
4553        }
4554      }
4555
4556      // bottom of the gallery (pagination, more button...)
4557      GalleryBottomManage();
4558
4559      var tnToDisplay = [];
4560      var tnToReDisplay = [];
4561      
4562      CacheViewport();
4563      G.GOM.clipArea.top = -1;
4564      cnt = 0 ;
4565      var lastTnIdx = -1;
4566      G.GOM.clipArea.height = 0;
4567      // NOTE: loop always the whole GOM.items --> in case an already displayed thumbnail needs to be removed
4568      for( var i = 0; i < nbTn ; i++ ) {
4569        let curTn = G.GOM.items[i];
4570        if( curTn.inDisplayArea ) {
4571          if( G.GOM.clipArea.top == -1 ) {
4572            G.GOM.clipArea.top = curTn.top;
4573          }
4574          if( (curTn.top - G.GOM.clipArea.top) <= -1 ) {
4575            // with mosaic layout, the first thumbnail may not give the top position
4576            G.GOM.clipArea.top = curTn.top;
4577          }
4578
4579          G.GOM.clipArea.height = Math.max( G.GOM.clipArea.height, curTn.top-G.GOM.clipArea.top + curTn.height);
4580        
4581          if( curTn.neverDisplayed ) {
4582						// thumbnail is not displayed -> check if in viewport to display or not
4583            var top = G.GOM.cache.containerOffset.top + (curTn.top - G.GOM.clipArea.top);
4584            // var left=containerOffset.left+curTn.left;
4585            if( (top + curTn.height) >= (G.GOM.cache.viewport.t - threshold) && top <= (G.GOM.cache.viewport.t + G.GOM.cache.viewport.h + threshold) ) {
4586              // build thumbnail
4587              let item = G.I[curTn.thumbnailIdx];
4588              if( item.$elt == null ) {
4589                // ThumbnailBuild( item, curTn.thumbnailIdx, i, (i+1) == nbTn );
4590                ThumbnailBuild( item, curTn.thumbnailIdx, i );
4591              }
4592              tnToDisplay.push({idx:i, delay:cnt, top: curTn.top, left: curTn.left});
4593              cnt++;
4594            }
4595          }
4596          else {
4597            tnToReDisplay.push({idx: i, delay: 0, top: curTn.top, left: curTn.left});
4598          }
4599          // G.GOM.itemsDisplayed++;
4600          lastTnIdx = i;
4601        }
4602        else {
4603          curTn.displayed = false;
4604          let item = G.I[curTn.thumbnailIdx];
4605          if( item.$elt != null ){
4606            item.$elt.css({ opacity: 0, display: 'none' });
4607          }
4608        }
4609      }
4610
4611      var areaWidth = G.$E.conTnParent.width();
4612
4613      // set gallery area really used size
4614      // if( G.GOM.displayArea.width != G.GOM.displayAreaLast.width || G.GOM.displayArea.height != G.GOM.displayAreaLast.height ) {
4615      if( G.GOM.displayArea.width != G.GOM.displayAreaLast.width || G.GOM.clipArea.height != G.GOM.displayAreaLast.height ) {
4616        G.$E.conTn.width( G.GOM.displayArea.width ).height( G.GOM.clipArea.height );
4617        G.GOM.displayAreaLast.width = G.GOM.displayArea.width;
4618        G.GOM.displayAreaLast.height = G.GOM.clipArea.height;
4619        // G.GOM.displayAreaLast.height=G.GOM.displayArea.height-G.GOM.clipArea.top;
4620      }
4621
4622      if( areaWidth != G.$E.conTnParent.width() ) {
4623        // gallery area width changed since layout calculation (for example when a scrollbar appeared)
4624        // so we need re-calculate the layout before displaying the thumbnails
4625        G.GOM.cache.areaWidth = G.$E.conTnParent.width();
4626        GallerySetLayout();
4627        GalleryDisplayPart1();
4628        GalleryDisplayPart2( forceTransition );
4629        return;
4630      }
4631
4632      // counter of not displayed images (is displayed on the last thumbnail)
4633      if( G.layout.support.rows ) {
4634        if( G.galleryDisplayMode.Get() == 'ROWS' || (G.galleryDisplayMode.Get() == 'FULLCONTENT' && G.galleryLastRowFull.Get() && G.GOM.lastFullRow != -1) ){
4635          if( lastTnIdx < (nbTn - 1) ) {
4636            G.GOM.lastDisplayedIdxNew = lastTnIdx;
4637          }
4638          else {
4639            G.GOM.lastDisplayedIdxNew =- 1;
4640          }
4641          // remove last displayed counter
4642          if( G.GOM.lastDisplayedIdx != -1 ) {
4643            let item = G.I[G.GOM.items[G.GOM.lastDisplayedIdx].thumbnailIdx];
4644            item.$getElt('.nGY2GThumbnailIconsFullThumbnail').html('');
4645          }
4646        }
4647      }
4648
4649      
4650      // batch set position (and display animation) to all thumbnails
4651      // first display newly built thumbnails
4652      
4653      G.GOM.thumbnails2Display=[];
4654      
4655      var duration = ThumbnailPreparePosition( tnToDisplay );
4656      ThumbnailPreparePosition( tnToReDisplay );
4657
4658      ThumbnailDisplayAnimBatch();
4659
4660      if( G.tn.opt.Get('displayTransition') == 'NONE' ) {
4661        G.galleryResizeEventEnabled = true;
4662        // GalleryThumbnailSliderBuildAndStart();  // image slider on last displayed thumbnail
4663        TriggerCustomEvent('galleryDisplayed');
4664      }
4665      else {
4666        // setTimeout(function() {
4667        requestTimeout( function() {
4668          // change value after the end of the display transistion of the newly built thumbnails
4669          G.galleryResizeEventEnabled = true;
4670          // GalleryThumbnailSliderBuildAndStart();  // image slider on last displayed thumbnail
4671          TriggerCustomEvent('galleryDisplayed');
4672        // }, nbBuild * G.tn.opt.Get('displayInterval'));
4673        }, duration * G.tn.opt.Get('displayInterval'));
4674      }
4675      
4676    }
4677    
4678    
4679    function ThumbnailPreparePosition( lstThumb ) {
4680
4681      var nbBuild = lstThumb.length;
4682      if( nbBuild == 0 ) { return 0; }
4683    
4684      
4685      var displayOrder = G.tn.opt.Get('displayOrder');
4686      
4687			if( displayOrder == 'random' ) {
4688				NGY2Tools.AreaShuffle( lstThumb );
4689			}
4690      else {
4691        if( displayOrder == 'rowByRow' && !( G.layout.engine == 'JUSTIFIED' || G.layout.engine == 'GRID' )) {
4692          displayOrder = '';
4693        }
4694        if( (displayOrder == 'colFromRight' || displayOrder == 'colFromLeft' ) && !(G.layout.engine == 'CASCADING' || G.layout.engine == 'GRID' )) {
4695          displayOrder = '';
4696        }
4697      }
4698     
4699        
4700      // DISPLAY COLUMN BY COLUMN
4701      if( displayOrder == 'colFromRight' || displayOrder == 'colFromLeft' ) {
4702        var tab = [];
4703        var cols = [];
4704        for( var i = 0; i < nbBuild ; i++ ) {
4705          if( tab[lstThumb[i].left] == undefined ) {
4706            tab[lstThumb[i].left] = [];
4707            cols.push( lstThumb[i].left );
4708          }
4709          tab[lstThumb[i].left].push( lstThumb[i].idx )
4710        }
4711        if( displayOrder == 'colFromRight' ) {
4712          cols = cols.reverse();
4713        }
4714        for( var i = 0; i < cols.length; i++ ) {
4715          var col = cols[i];
4716          for( var j = 0; j < tab[col].length; j++ ) {
4717            ThumbnailSetPosition( tab[col][j], i);
4718          }
4719        }
4720        return(i);
4721      }
4722        
4723        
4724      // STANDARD DISPLAY OR ROW BY ROW
4725      var d = 0;
4726      var top = lstThumb[0].top;
4727      for( var i = 0; i < nbBuild ; i++ ) {
4728        // ThumbnailSetPosition(tnToDisplay[i].idx, tnToDisplay[i].delay+10);
4729        // ThumbnailSetPosition(tnToDisplay[i].idx, i);
4730
4731        if( displayOrder == 'rowByRow' ) {
4732          // DISPLAY ROW BY ROW
4733          if( lstThumb[i].top > top ) {
4734            d++;
4735            top = lstThumb[i].top;
4736          }
4737        }
4738        else {
4739          d++;
4740        }
4741        ThumbnailSetPosition(lstThumb[i].idx, d);
4742      }
4743      return(d);
4744      
4745    }
4746    
4747    // Thumbnail: set the new position
4748    function ThumbnailSetPosition( GOMidx, cnt ) {
4749      var newTop=   0;
4750      var curTn=    G.GOM.items[GOMidx];
4751      var idx=      G.GOM.items[GOMidx].thumbnailIdx;
4752      var item=     G.I[idx];
4753    
4754      if( curTn.neverDisplayed ) {
4755        // thumbnail is built but has never been displayed (=first display)
4756        var top = curTn.top - G.GOM.clipArea.top;
4757        if( G.tn.opt.Get('stacks') > 0 ) {
4758          // we have stacks -> do not display them here. They will be displayed at the end of the display animation
4759          item.$elt.last().css({ display: 'block'});
4760          item.$elt.css({ top: top , left: curTn.left });
4761        }
4762        else {
4763          item.$elt.css({ display: 'block', top: top , left: curTn.left });
4764        }
4765        newTop=top;
4766        
4767        // display the image of the thumbnail when fully loaded
4768        if( G.O.thumbnailWaitImageLoaded === true ) {
4769          var gi_imgLoad = ngimagesLoaded( item.$getElt('.nGY2TnImg2') );
4770          gi_imgLoad.on( 'progress', function( instance, image ) {
4771            if( image.isLoaded ) {
4772              var albumIdx = image.img.getAttribute('data-albumidx');
4773              if( albumIdx == G.GOM.albumIdx ) {
4774                // ignore event if not on current album
4775                var idx = image.img.getAttribute('data-idx');
4776                G.I[idx].ThumbnailImageReveal();
4777              }
4778            }
4779          });
4780        }
4781        // display the thumbnail
4782        ThumbnailAppear(GOMidx, cnt);
4783
4784        curTn.displayed = true;
4785        curTn.neverDisplayed = false;
4786      }
4787      else {
4788        var topOld = G.GOM.cache.containerOffset.top + item.top;
4789        var top = G.GOM.cache.containerOffset.top + (curTn.top - G.GOM.clipArea.top);
4790        newTop = curTn.top - G.GOM.clipArea.top;
4791        var vp = G.GOM.cache.viewport;
4792        if( G.O.thumbnailDisplayOutsideScreen || ( ( (topOld + curTn.height) >= (vp.t - vp.h) && topOld <= (vp.t + vp.h * 4) ) ||
4793              ( (top + curTn.height) >= (vp.t - vp.h) && top <= (vp.t + vp.h * 4) ) )  ) {
4794          // thumbnail positioned in enlarged viewport (viewport + 4 x viewport height) (v1.5: changed from 2 to 4)
4795          if( curTn.displayed ) {
4796            // thumbnail is displayed
4797            if( item.top != curTn.top || item.left != curTn.left ) {
4798              // set position
4799              if( G.O.galleryResizeAnimation == true ) {
4800                // with transition
4801                var tweenable = new NGTweenable();
4802                tweenable.tween({
4803                  from:       { top: item.top, left: item.left,  height: item.height,  width: item.width },
4804                  to:         { top: newTop,   left: curTn.left, height: curTn.height, width: curTn.width },
4805                  attachment: { $e: item.$elt },
4806                  duration:   100,
4807                  delay:      cnt * G.tn.opt.Get('displayInterval') / 5,
4808                  // easing:     'easeInOutQuad',
4809                  easing:     'easeOutQuart',
4810                  step:       function (state, att) {
4811                    // window.ng_draf( function() {
4812                      att.$e.css(state);
4813                    // });
4814                  },
4815                  finish:     function (state, att) {
4816                    var _this=this;
4817                    // window.ng_draf( function() {
4818                      _this.dispose();
4819                    // });
4820                  }
4821                });
4822              }
4823              else {
4824                // set position without transition
4825                // item.$elt.css({ top: curTn.top , left: curTn.left });
4826                item.$elt.css({ top: newTop , left: curTn.left });
4827              }
4828            }
4829          }
4830          else {
4831            // re-display thumbnail
4832            curTn.displayed = true;
4833            // item.$elt.css({ display: 'block', top: curTn.top , left: curTn.left, opacity:1 });
4834            item.$elt.css({ display: 'block', top: newTop, left: curTn.left, opacity: 1 });
4835            ThumbnailAppearFinish(item);
4836          }
4837        }
4838        else {
4839          // undisplay thumbnail if not in viewport+margin --> performance gain
4840          curTn.displayed = false;
4841          item.$elt.css({ display: 'none'});
4842        }
4843      }
4844      item.left = curTn.left;
4845      item.top = newTop;
4846      
4847      // set new size if changed
4848      if( item.width != curTn.width || item.height != curTn.height ) {
4849        item.$elt.css({ width: curTn.width , height: curTn.height });
4850        item.width = curTn.width;
4851        item.height = curTn.height;
4852        
4853        // if( curTn.resizedContentWidth > 0 ) {
4854        // resize also the content (=image)
4855        if( item.resizedContentWidth != curTn.resizedContentWidth || item.resizedContentHeight != curTn.resizedContentHeight ) {
4856          if( item.kind == 'albumUp' ) {
4857            // item.$getElt('.nGY2GThumbnailAlbumUp').css({'height': curTn.resizedContentHeight, 'width': curTn.resizedContentWidth});
4858          }
4859          else {
4860            item.$getElt('.nGY2GThumbnailImage').css({'height': curTn.resizedContentHeight, 'width': curTn.resizedContentWidth});
4861
4862            if( G.layout.engine == 'JUSTIFIED'  ) {
4863              item.$getElt('.nGY2GThumbnailImg').css({'height': curTn.resizedContentHeight, 'width': curTn.resizedContentWidth});
4864            }
4865          }
4866          item.resizedContentWidth = curTn.resizedContentWidth;
4867          item.resizedContentHeight = curTn.resizedContentHeight;
4868        }
4869      }
4870      
4871      
4872      // add counter of remaining (not displayed) images 
4873      if( G.GOM.lastDisplayedIdxNew == GOMidx &&  G.layout.support.rows ) {
4874        if( (G.galleryDisplayMode.Get() == 'ROWS' && G.galleryMaxRows.Get() > 0) || (G.galleryDisplayMode.Get() == 'FULLCONTENT' && G.galleryLastRowFull.Get() && G.GOM.lastFullRow != -1) ){
4875          // number of items
4876          var nb = G.GOM.items.length - GOMidx - 1;
4877          if( item.albumID != '0' && G.O.thumbnailLevelUp ) {
4878            nb--;
4879          }
4880
4881          if( nb > 0 ) {
4882            // display counter
4883            if( G.O.thumbnailOpenInLightox || G.O.thumbnailSliderDelay > 0  ) {
4884              item.$getElt('.nGY2GThumbnailIconsFullThumbnail').html( '+' + nb);
4885            }
4886
4887            // if( G.layout.engine == 'GRID' && G.GOM.slider.hostItem != G.GOM.NGY2Item(GOMidx) ) {
4888            // image slider on last displayed thumbnail
4889            if( G.O.thumbnailLabel.get('position') != 'right' && G.O.thumbnailLabel.get('position') != 'left' ) {
4890              if( G.GOM.slider.hostItem != G.GOM.NGY2Item(GOMidx) ) {
4891
4892                // set current slider back to initial content
4893                GalleryThumbnailSliderSetContent( G.GOM.slider.hostItem );
4894                // new slider
4895                G.GOM.slider.hostIdx = GOMidx;
4896                G.GOM.slider.hostItem = G.GOM.NGY2Item(GOMidx);
4897                G.GOM.slider.nextIdx = GOMidx;
4898                G.GOM.slider.currentIdx = GOMidx;
4899                GalleryThumbnailSliderBuildAndStart();  // image slider on last displayed thumbnail
4900                // GalleryThumbnailSliderSetNextContent();
4901              }
4902            }
4903          }
4904          else {
4905            // reset slider content to initial content because all thumbnails are displayed
4906            GalleryThumbnailSliderSetContent( G.GOM.slider.hostItem );
4907            G.GOM.slider.hostIdx = -1;
4908          }
4909          
4910          G.GOM.lastDisplayedIdx = GOMidx;
4911        }
4912      }
4913
4914    }
4915    
4916    // ---------------------
4917    // replace image on last thumbnails with not displayed ones (mode ROWS or FULLCONTENT with galleryLastRowFull enabled)
4918    // function GalleryLastThumbnailSlideImage() {
4919    function GalleryThumbnailSliderBuildAndStart() {
4920
4921      if( G.O.thumbnailSliderDelay == 0 || G.GOM.slider.hostIdx == -1 ) {
4922        return;
4923      }
4924      clearTimeout(G.GOM.slider.timerID);
4925      
4926      var item = G.GOM.slider.hostItem;
4927
4928      // dupplicate image layer -> for the next image
4929      if( item.$getElt('.nGY2TnImgNext').length == 0 ) {
4930        item.$getElt('.nGY2TnImg').clone().removeClass('nGY2TnImg').addClass('nGY2TnImgNext').insertAfter(item.$getElt('.nGY2TnImg'));
4931        item.$getElt('.nGY2TnImgBack').clone().removeClass('nGY2TnImgBack').addClass('nGY2TnImgBackNext').insertAfter(item.$getElt('.nGY2TnImg', true));
4932        item.$getElt('.nGY2GThumbnailImage', true); // important -> refresh the cache
4933        item.$getElt('.nGY2GThumbnailImg', true);   // important -> refresh the cache
4934      }
4935
4936      item.CSSTransformSet('.nGY2TnImgNext', 'translateX', '100%', true);
4937      item.CSSTransformApply( '.nGY2TnImgNext' );
4938      item.CSSTransformSet('.nGY2TnImgBackNext', 'translateX', '100%', true);
4939      item.CSSTransformApply( '.nGY2TnImgBackNext' );
4940
4941      GalleryThumbnailSliderSetNextContent();
4942      
4943      // clearTimeout(G.GOM.slider.timerID);
4944      // G.GOM.slider.timerID = setTimeout(function(){ GalleryThumbnailSliderStartTransition() }, G.O.thumbnailSliderDelay);
4945      G.GOM.slider.timerID = requestTimeout(function(){ GalleryThumbnailSliderStartTransition() }, G.O.thumbnailSliderDelay);
4946    }
4947
4948    
4949    function GalleryThumbnailSliderSetNextContent() {
4950
4951      G.GOM.slider.nextIdx++;
4952      if( G.GOM.slider.nextIdx >= G.GOM.items.length ) {
4953        G.GOM.slider.nextIdx = G.GOM.slider.hostIdx;
4954      }
4955      
4956      // new image
4957      var newItem = G.GOM.NGY2Item(G.GOM.slider.nextIdx);
4958      // var imgBlurred = G.emptyGif;
4959      var bgImg = "url('" + G.emptyGif + "')";
4960      if( newItem.imageDominantColors != null ) {
4961        // imgBlurred = newItem.imageDominantColors;
4962        bgImg = "url('" + newItem.imageDominantColors + "')";
4963      }
4964      G.GOM.slider.hostItem.$getElt('.nGY2TnImgBackNext', true).css({'background-image': bgImg, opacity: 1 });
4965      G.GOM.slider.hostItem.$getElt('.nGY2TnImgNext', true).css({ 'background-image': "url('" + newItem.thumbImg().src + "')", opacity: 1 });
4966      G.GOM.slider.hostItem.$getElt('.nGY2TnImgNext .nGY2GThumbnailImg', true).attr('src', newItem.thumbImg().src );
4967      
4968
4969    }
4970    
4971    // thumbnail slider - transition from one image to the next one
4972    function GalleryThumbnailSliderStartTransition() {
4973      
4974      if( G.GOM.slider.hostItem.$getElt() != null ) {
4975
4976        // slider transition
4977        var tweenable = new NGTweenable();
4978        G.GOM.slider.tween = tweenable;
4979        tweenable.tween({
4980          from:         { 'left': 100 },
4981          to:           { 'left': 0 },
4982          duration:     800,
4983          delay:        0,
4984          // easing:       'easeInOutQuad',
4985          easing:       'easeOutQuart',
4986          
4987          step: function (state) {
4988            if( G.GOM.slider.hostItem.$getElt() == null ) {
4989              // the thumbnail may have been destroyed since the start of the animation
4990              G.GOM.slider.tween.stop(false);
4991              return;
4992            }
4993
4994            // window.ng_draf( function() {
4995              // slide current content
4996              G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImgBack', 'translateX', -(100 - state.left) + '%');
4997              G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImgBack' );
4998              G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImg', 'translateX', -(100 - state.left) + '%');
4999              G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImg' );
5000
5001              // slide new content
5002              G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImgBackNext', 'translateX', state.left + '%');
5003              G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImgBackNext' );
5004              G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImgNext', 'translateX', state.left + '%');
5005              G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImgNext' );
5006            // });
5007
5008            
5009          },
5010          finish: function (state) {
5011            if( G.GOM.slider.hostItem.$getElt() == null ) {
5012              // the thumbnail may be destroyed since the start of the animation
5013              return;
5014            }
5015           
5016            if( G.GOM.NGY2Item(G.GOM.slider.nextIdx) == null ) { return; } // item does not exist anymore
5017            
5018            // window.ng_draf( function() {
5019              // set new content as current content
5020              GalleryThumbnailSliderSetContent( G.GOM.NGY2Item(G.GOM.slider.nextIdx) );
5021              G.GOM.slider.currentIdx = G.GOM.slider.nextIdx;
5022              GalleryThumbnailSliderSetNextContent();
5023              
5024              clearTimeout(G.GOM.slider.timerID);
5025              // G.GOM.slider.timerID=setTimeout(function(){ GalleryThumbnailSliderStartTransition() }, G.O.thumbnailSliderDelay);
5026              G.GOM.slider.timerID = requestTimeout(function(){ GalleryThumbnailSliderStartTransition() }, G.O.thumbnailSliderDelay);
5027            // });
5028          }
5029        });
5030      }
5031    }
5032    
5033    // set main content of the thumbnail hosting the slider
5034    // hide the elements for the next content of the slider
5035    function GalleryThumbnailSliderSetContent( ngy2itemContent ) {
5036        if( G.GOM.slider.hostIdx == -1 ) { return; }
5037        
5038        if( G.GOM.slider.tween != null ) {
5039          if( G.GOM.slider.tween._isTweening  == true ) {
5040            G.GOM.slider.tween.stop(false);
5041          }
5042        }
5043
5044        var bgImg = "url('" + G.emptyGif + "')";
5045        if( ngy2itemContent.imageDominantColors != null ) {
5046          bgImg = "url('" + ngy2itemContent.imageDominantColors + "')";
5047        }
5048        G.GOM.slider.hostItem.$getElt('.nGY2TnImgBack').css('background-image', bgImg);
5049        G.GOM.slider.hostItem.$getElt('.nGY2TnImg').css('background-image', "url('" + ngy2itemContent.thumbImg().src + "')" );
5050        G.GOM.slider.hostItem.$getElt('.nGY2TnImg .nGY2GThumbnailImg').attr('src', ngy2itemContent.thumbImg().src );
5051        
5052        G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImgBack', 'translateX', '0');
5053        G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImgBack' );
5054        G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImg', 'translateX', '0');
5055        G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImg' );
5056
5057        // place the containers for the next image slider outside of the thumbnail (=hidden)
5058        G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImgBackNext', 'translateX', '100%', true);
5059        G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImgBackNext' );
5060        G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImgNext', 'translateX', '100%', true);
5061        G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImgNext' );
5062
5063        // set new title and description
5064        if( G.O.thumbnailLabel.get('display') == true ) {
5065          var icons = G.O.icons.thumbnailAlbum;
5066          if( ngy2itemContent.kind != 'album' ) {
5067            icons = G.O.icons.thumbnailImage;
5068          }
5069          G.GOM.slider.hostItem.$getElt('.nGY2GThumbnailTitle').html(icons + getThumbnailTitle(ngy2itemContent));
5070          G.GOM.slider.hostItem.$getElt('.nGY2GThumbnailDescription').html(icons + getTumbnailDescription(ngy2itemContent));
5071        }
5072      }
5073    
5074   
5075    
5076    // Compute the height of the label part of a thumbnail (title+description, both single line)
5077    function ThumbnailGetLabelHeight() {
5078      var newElt = [],
5079      newEltIdx =  0;
5080
5081      // if( G.O.thumbnailLabel.get('display') == false && G.tn.toolbar.getWidth(item) <= 0 ) {
5082      if( G.O.thumbnailLabel.get('display') == false  ) {
5083        return 0;
5084      }
5085      
5086      // var desc='';
5087      // if( G.O.thumbnailLabel.get('displayDescription') == true ) {
5088        // desc = 'aAzZjJ';
5089      // }
5090
5091      // visibility set to hidden
5092      newElt[newEltIdx++] = '<div class="nGY2GThumbnail ' + G.O.theme + '" style="display:block;visibility:hidden;position:absolute;top:-9999px;left:-9999px;" ><div class="nGY2GThumbnailSub">';
5093      if( G.O.thumbnailLabel.get('display') == true ) {
5094        // Labels: title and description
5095        newElt[newEltIdx++] = '  <div class="nGY2GThumbnailLabel" '+ G.tn.style.getLabel() +'>';
5096        newElt[newEltIdx++] = '    <div class="nGY2GThumbnailAlbumTitle" '+G.tn.style.getTitle()+'>aAzZjJ</div>';
5097        if( G.O.thumbnailLabel.get('displayDescription') == true ) {
5098          newElt[newEltIdx++] = '    <div class="nGY2GThumbnailDescription" '+G.tn.style.getDesc()+'>'+'aAzZjJ'+'</div>';
5099        }
5100        newElt[newEltIdx++] = '  </div>';
5101      }
5102      
5103      newElt[newEltIdx++] = '</div></div>';
5104    
5105      var $newDiv = jQuery(newElt.join('')).appendTo(G.$E.conTn);
5106      var h = $newDiv.find('.nGY2GThumbnailLabel').outerHeight(true);
5107      $newDiv.remove();
5108
5109      return h;
5110    }
5111    
5112    function ThumbnailBuildStacks( bgColor ) {
5113      var ns=G.tn.opt.Get('stacks');
5114      if( ns == 0 ) { return ''; }
5115     
5116      var s='';
5117      for( var i=0; i<ns; i++ ) {
5118        s='<div class="nGY2GThumbnailStack " style="display:none;'+bgColor+'"></div>'+s;
5119      }
5120      return s;
5121    }
5122    
5123    //----- Build one UP thumbnail (=navigation thumbnail)
5124    function ThumbnailBuildAlbumpUp( item, GOMidx ) {
5125    // function ThumbnailBuildAlbumpUp( item, idx, GOMidx ) {
5126      var newElt = [],
5127      newEltIdx = 0;
5128      
5129      var mp = '';
5130      if( G.O.thumbnailOpenInLightox === false ) {
5131        mp = 'cursor:default;'
5132      }
5133      
5134      newElt[newEltIdx++] = ThumbnailBuildStacks('') + '<div class="nGY2GThumbnail" style="display:none;opacity:0;' + mp + '" >';
5135      newElt[newEltIdx++] = '  <div class="nGY2GThumbnailSub">';
5136
5137      var h=G.tn.defaultSize.getHeight(),
5138      w=G.tn.defaultSize.getWidth();
5139
5140      newElt[newEltIdx++] = '    <div class="nGY2GThumbnailImage" style="width:'+w+'px;height:'+h+'px;"><img class="nGY2GThumbnailImg" src="'+G.emptyGif+'" alt="" style="max-width:'+w+'px;max-height:'+h+'px;" ></div>';
5141      // newElt[newEltIdx++] = '    <div class="nGY2GThumbnailAlbumUp" style="width:'+w+'px;height:'+h+'px;">'+G.O.icons.thumbnailAlbumUp+'</div>';
5142      newElt[newEltIdx++] = '    <div class="nGY2GThumbnailAlbumUp" >'+G.O.icons.thumbnailAlbumUp+'</div>';
5143      newElt[newEltIdx++] = '  </div>';
5144      newElt[newEltIdx++] = '</div>';
5145      
5146      var $newDiv = jQuery(newElt.join('')).appendTo(G.$E.conTn); //.animate({ opacity: 1},1000, 'swing');  //.show('slow'); //.fadeIn('slow').slideDown('slow');
5147      
5148      item.$elt = $newDiv;
5149      $newDiv.data('index', GOMidx);
5150      item.$getElt('.nGY2GThumbnailImg').data('index', GOMidx);
5151      
5152      return;
5153    }
5154
5155    
5156    //----- Build one thumbnail
5157    function ThumbnailBuild( item, idx, GOMidx ) {
5158    // function ThumbnailBuild( item, idx, GOMidx, lastOne ) {
5159      item.eltTransform =  [];
5160      item.eltFilter =     [];
5161      item.hoverInitDone = false;
5162      item.$Elts =         [];
5163
5164      if( item.kind == 'albumUp' ) {
5165        ThumbnailBuildAlbumpUp( item, GOMidx);
5166        return;
5167      }
5168
5169      var newElt = [],
5170      newEltIdx = 0;
5171
5172      var mp = '';
5173      if( G.O.thumbnailOpenInLightox === false ) {
5174        mp = 'cursor:default;'
5175      }
5176
5177      // var src = encodeURI(item.thumbImg().src),
5178     
5179      var src = (item.thumbImg().src).replace(/'/g, "%27"),   // replace single quote with %27
5180      sTitle = getThumbnailTitle(item);
5181
5182      // image background -> visible during image download
5183      var bg = '';
5184      var bgImg = "background-image: url('" + G.emptyGif + "');";
5185      if( item.imageDominantColors != null ) {
5186        // dominant colorS (blurred preview image)
5187        bgImg = "background-image: url('" + item.imageDominantColors + "');";
5188      }
5189      else {
5190        // dominant color -> background color
5191        if( item.imageDominantColor != null ) {
5192          bg = 'background-color:' + item.imageDominantColor + ';';
5193        }
5194        else {
5195          bgImg = '';
5196        }
5197      }
5198
5199      var op = 'opacity:1;';
5200      if( G.O.thumbnailWaitImageLoaded == true ) {
5201        op = 'opacity:0;';
5202      }
5203
5204      // ##### thumbnail containers  (with stacks)
5205      newElt[newEltIdx++] = ThumbnailBuildStacks(bg) + '<div class="nGY2GThumbnail nGY2GThumbnail_'+G.GOM.curNavLevel+'" style="display:none;opacity:0;' + mp + '"><div class="nGY2GThumbnailSub ' + ( G.O.thumbnailSelectable && item.selected ? "nGY2GThumbnailSubSelected" : "" ) + '">';
5206
5207      
5208      // image size
5209      var w = G.tn.settings.getW();
5210      var h = G.tn.settings.getH();
5211      if( G.tn.settings.getMosaic() !== null ) {
5212        // mosaic layout -> 
5213        w = G.GOM.items[GOMidx].width;
5214        h = G.GOM.items[GOMidx].height;
5215      }
5216
5217      var bgSize = 'contain';
5218      if( G.tn.opt.Get('crop') ) {
5219        bgSize = 'cover';             // thumbnail image will be cropped to fit in the thumbnail (no black border)
5220      }
5221      
5222      // ##### layer for image background (color, dominant color, blurred preview)
5223      var s1 = "position: absolute; top: 0px; left: 0px; width:" + w + "px; height:" + h + "px;"+ bg + bgImg + " background-position: center center;  background-repeat: no-repeat; background-size:" + bgSize + "; overflow: hidden;";
5224      newElt[newEltIdx++]='<div class="nGY2GThumbnailImage nGY2TnImgBack" style="' + s1 + '"></div>';
5225      
5226      // #### layer for image 
5227      // for url in CSS: single backslashes are replaced by double backslashes
5228      var s2 = op + "position: absolute; top: 0px; left: 0px; width:" + w + "px; height:" + h + "px; background-image: url('" + src.replace(/\\/g, '\\\\') + "'); background-position: center center; background-repeat: no-repeat; background-size:" + bgSize + "; overflow: hidden;";
5229      newElt[newEltIdx++]='<div class="nGY2GThumbnailImage nGY2TnImg" style="' + s2 + '">';
5230      newElt[newEltIdx++]='  <img class="nGY2GThumbnailImg nGY2TnImg2" src="' + src + '" alt="' + sTitle + '" style="opacity:0;" data-idx="' + idx + '" data-albumidx="' + G.GOM.albumIdx + '" >';
5231      newElt[newEltIdx++]='</div>';
5232      
5233      // ##### layer for user customization purposes
5234      newElt[newEltIdx++]='<div class="nGY2GThumbnailCustomLayer"></div>';
5235
5236      // ##### layer for labels (title + description and their icons)
5237      if( G.O.thumbnailLabel.get('display') == true ) {
5238        // Labels: title and description
5239        newElt[newEltIdx++]= '  <div class="nGY2GThumbnailLabel" '+ G.tn.style.getLabel(item) + '>';
5240        if( item.kind == 'album' ) {
5241          // album kind
5242          newElt[newEltIdx++]= '    <div class="nGY2GThumbnailTitle nGY2GThumbnailAlbumTitle" ' + G.tn.style.getTitle() + '>' + G.O.icons.thumbnailAlbum + sTitle + '</div>';
5243        }
5244        else {
5245          // image/media kind
5246          newElt[newEltIdx++]= '    <div class="nGY2GThumbnailTitle nGY2GThumbnailImageTitle" ' + G.tn.style.getTitle() + '>' + G.O.icons.thumbnailImage + sTitle + '</div>';
5247        }
5248        newElt[newEltIdx++]= '    <div class="nGY2GThumbnailDescription" ' + G.tn.style.getDesc() + '>' + getTumbnailDescription(item) + '</div>';
5249        newElt[newEltIdx++]= '  </div>';
5250      }
5251
5252      // ##### layer for tools
5253      // newElt[newEltIdx++] = ThumbnailBuildTools(item, lastOne);
5254      newElt[newEltIdx++] = ThumbnailBuildTools(item);
5255      
5256      // close containers
5257      newElt[newEltIdx++]='</div></div>';
5258      
5259      var $newDiv =jQuery(newElt.join('')).appendTo(G.$E.conTn);
5260
5261      item.$elt=$newDiv;
5262      $newDiv.data('index',GOMidx);
5263      item.$getElt('.nGY2GThumbnailImg').data('index',GOMidx);
5264      
5265      // Custom init function
5266      var fu=G.O.fnThumbnailInit;
5267      if( fu !== null ) {
5268        typeof fu == 'function' ? fu($newDiv, item, GOMidx) : window[fu]($newDiv, item, GOMidx);
5269      }
5270
5271      if( item.title != 'image gallery by nanogallery2 [build]' ) {
5272        ThumbnailOverInit(GOMidx);
5273      }
5274      
5275      return ;
5276    }
5277
5278    
5279    // Thumbnail layer for tools (toolbars and counter)
5280    function ThumbnailBuildTools( item ) {
5281    
5282      // toolbars
5283      var tb = ThumbnailBuildToolbarOne(item, 'topLeft') + ThumbnailBuildToolbarOne(item, 'topRight') + ThumbnailBuildToolbarOne(item, 'bottomLeft') + ThumbnailBuildToolbarOne(item, 'bottomRight');
5284      
5285      // counter of not displayed images
5286      tb += '<div class="nGY2GThumbnailIconsFullThumbnail"></div>';
5287
5288      return tb;
5289    }
5290    
5291    function ThumbnailBuildToolbarOne( item, position ) {
5292      var toolbar = '';
5293      var tb =      G.tn.toolbar.get(item);
5294      var width =   { xs:0, sm:1, me:2, la:3, xl:4 };
5295      var cnt =     0;
5296      
5297      if( tb[position] != '' ) {
5298        var pos='top: 0; right: 0; text-align: right;';     // 'topRight' and default
5299        switch( position ) {
5300          case 'topLeft':
5301            pos = 'top: 0; left: 0; text-align: left;';
5302            break;
5303          case 'bottomRight':
5304            pos = 'bottom: 0; right: 0; text-align: right;';
5305            break;
5306          case 'bottomLeft':
5307            pos = 'bottom: 0; left: 0; text-align: left;';
5308            break;
5309        }
5310        
5311        toolbar += '  <ul class="nGY2GThumbnailIcons" style="' + pos + '">';
5312        
5313        var icons = tb[position].split(',');
5314        var nb = icons.length;
5315        for( var i = 0; i < nb; i++ ) {
5316          var icon = icons[i].replace(/^\s*|\s*$/, '');   //trim trailing/leading whitespace
5317
5318          var minWidth = icon.substring(0,2).toLowerCase();
5319          var tIcon = icon;
5320          var display = true;
5321          if( /xs|sm|me|la|xl/i.test(minWidth) ) {
5322            // check visbility (depending on screen width)
5323            if( width[minWidth] > width[G.GOM.curWidth] ) {
5324              display = false;
5325            }
5326            tIcon = icon.substring(2);
5327          }
5328          
5329          if( display ) {
5330            var sp=(i+1<nb ? '&nbsp;' :'');
5331            switch( tIcon ) {
5332              case 'COUNTER':
5333                if( item.kind == 'album' ) {
5334                  toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="">';
5335                  toolbar += '      <div class="nGY2GThumbnailIconImageCounter"></div>';
5336                  toolbar += '      <div class="nGY2GThumbnailIconText">' + G.O.icons.thumbnailCounter+Math.max((item.getContentLength(false)),item.numberItems) + sp + '</div>';
5337                  toolbar += '    </li>';
5338                  cnt++;
5339                }
5340                break;
5341              case 'COUNTER2':
5342                if( item.kind == 'album' ) {
5343                  toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="">';
5344                  toolbar += '      <div class="nGY2GThumbnailIconTextBadge">' + G.O.icons.thumbnailCounter+Math.max((item.getContentLength(false)),item.numberItems) + sp + '</div>';
5345                  toolbar += '    </li>';
5346                  cnt++;
5347                }
5348                break;
5349              case 'SHARE':
5350                toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="' + tIcon + '">';
5351                toolbar += '      <div>' + G.O.icons.thumbnailShare + '</div>';
5352                toolbar += '    </li>';
5353                cnt++;
5354                break;
5355              case 'DOWNLOAD':
5356                toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="' + tIcon + '">';
5357                toolbar += '      <div>' + G.O.icons.thumbnailDownload + '</div>';
5358                toolbar += '    </li>';
5359                cnt++;
5360                break;
5361              case 'INFO':
5362                toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="' + tIcon + '">';
5363                toolbar += '      <div>' + G.O.icons.thumbnailInfo + '</div>';
5364                toolbar += '    </li>';
5365                cnt++;
5366                break;
5367              case 'SHOPPINGCART':
5368                toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="' + tIcon + '">';
5369                // toolbar += '      <div>' + G.O.icons.thumbnailShoppingcart + '</div>';
5370                toolbar += ThumbnailBuildToolbarOneCart( item );
5371                
5372                toolbar += '    </li>';
5373                cnt++;
5374                break;
5375              case 'DISPLAY':
5376                toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="DISPLAY">';
5377                toolbar += '      <div class="nGY2GThumbnailIconImageShare">' + G.O.icons.thumbnailDisplay + '</div>';
5378                toolbar += '    </li>';
5379                cnt++;
5380                break;
5381              case 'CUSTOM1':
5382              case 'CUSTOM2':
5383              case 'CUSTOM3':
5384              case 'CUSTOM4':
5385              case 'CUSTOM5':
5386              case 'CUSTOM6':
5387              case 'CUSTOM7':
5388              case 'CUSTOM8':
5389              case 'CUSTOM9':
5390              case 'CUSTOM10':
5391                var cust = tIcon.replace('CUSTOM', '');
5392                toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="' + tIcon.toLowerCase() + '">';
5393                toolbar += '      <div class="nGY2GThumbnailIconImageShare">' + G.O.icons['thumbnailCustomTool' + cust] + '</div>';
5394                toolbar += '    </li>';
5395                cnt++;
5396                break;
5397              case 'FEATURED':
5398                if( item.featured === true ) {
5399                  toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="">';
5400                  toolbar += '      <div class="nGY2GThumbnailIconImageFeatured">' + G.O.icons.thumbnailFeatured + '</div>';
5401                  toolbar += '    </li>';
5402                  cnt++;
5403                }
5404                break;
5405              case 'SELECT':
5406                if( G.O.thumbnailSelectable == true ) {
5407                  toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="TOGGLESELECT">';
5408                  if( item.selected === true ) {
5409                    toolbar += '      <div class="nGY2GThumbnailIconImageSelect nGY2ThumbnailSelected">' + G.O.icons.thumbnailSelected + '</div>';
5410                  }
5411                  else {
5412                    toolbar += '      <div class="nGY2GThumbnailIconImageSelect nGY2ThumbnailUnselected">' + G.O.icons.thumbnailUnselected + '</div>';
5413                  }
5414                  toolbar += '    </li>';
5415                  cnt++;
5416                }
5417                break;
5418            }
5419          }
5420        }
5421        toolbar += '  </ul>';
5422      }
5423      
5424      if( cnt > 0 ) {
5425        return toolbar;
5426      }
5427      else {
5428        return '';
5429      }
5430    }
5431    
5432    // CART ICON AND COUNTER
5433    function ThumbnailBuildToolbarOneCart( item ) {
5434      var q = 0;
5435      
5436      var id = item.GetID()
5437      for( var i=0; i<G.shoppingCart.length; i++ ) {
5438        if( G.I[G.shoppingCart[i].idx].GetID() == id ) {
5439          q = G.shoppingCart[i].qty;
5440        }
5441      }
5442      if( q == 0 ) {
5443        q = '';
5444      }
5445
5446      return '      <div>' + G.O.icons.thumbnailShoppingcart + q + '</div>';
5447    }
5448    function ThumbnailBuildToolbarOneCartUpdate( item ) {
5449      var $e = item.$elt;
5450
5451      if( $e != null ) {
5452        var $q = $e.find('*[data-ngy2action="SHOPPINGCART"]');
5453        if( $q !== undefined ) {
5454          $q.html( ThumbnailBuildToolbarOneCart( item ) );
5455        }
5456      }
5457    }
5458    
5459    function getThumbnailTitle( item ) {
5460    
5461      var sTitle = item.title;
5462      if( G.O.thumbnailLabel.get('display') == true ) {
5463        if( sTitle === undefined || sTitle.length == 0 ) { sTitle = '&nbsp;'; }
5464
5465        if( G.i18nTranslations.thumbnailImageTitle != '' ) {
5466          sTitle = G.i18nTranslations.thumbnailImageTitle;
5467        }
5468        var ml = G.O.thumbnailLabel.get('titleMaxLength');
5469        if( ml > 3 && sTitle.length > ml ){
5470          sTitle = sTitle.substring(0, ml) + '...';
5471        }
5472      }
5473      
5474      return sTitle;
5475    }
5476
5477    function getTumbnailDescription( item ) {
5478      var sDesc = '';
5479      if( G.O.thumbnailLabel.get('displayDescription') == true ) { 
5480        if( item.kind == 'album' ) {
5481          if( G.i18nTranslations.thumbnailImageDescription != '' ) {
5482            sDesc = G.i18nTranslations.thumbnailAlbumDescription;
5483          }
5484          else {
5485            sDesc = item.description;
5486          }
5487        }
5488        else {
5489          if( G.i18nTranslations.thumbnailImageDescription != '' ) {
5490            sDesc = G.i18nTranslations.thumbnailImageDescription;
5491          }
5492          else {
5493            sDesc = item.description;
5494          }
5495        }
5496        var ml = G.O.thumbnailLabel.get('descriptionMaxLength');
5497        if( ml > 3 && sDesc.length > ml ){
5498          sDesc = sDesc.substring(0, ml) + '...';
5499        }
5500        if( sDesc.length == 0 ) {
5501          sDesc = '&nbsp;';
5502        }
5503      }
5504      
5505      return sDesc;
5506    }
5507
5508    
5509    
5510    // Retrieve the maximum number of thumbnails that fits in one row
5511    function NbThumbnailsPerRow( areaWidth ) {
5512      var tnW = G.tn.defaultSize.getOuterWidth();
5513      
5514      var nbMaxTn = 0;
5515      if( G.O.thumbnailAlignment == 'justified' ) {
5516        nbMaxTn = Math.floor((areaWidth)/(tnW));
5517      }
5518      else {
5519        nbMaxTn = Math.floor((areaWidth + G.tn.settings.GetResponsive('gutterWidth'))/(tnW + G.tn.settings.GetResponsive('gutterWidth')));
5520      }
5521      
5522      if(  G.O.maxItemsPerLine >0 && nbMaxTn >  G.O.maxItemsPerLine ) {
5523        nbMaxTn = G.O.maxItemsPerLine;
5524      }
5525      
5526      if( nbMaxTn < 1 ) { nbMaxTn = 1; }
5527      
5528      return nbMaxTn
5529    }
5530  
5531    // Thumbnail display animation
5532    function ThumbnailAppear( n, cnt ) {
5533      var curTn = G.GOM.items[n];
5534      var item = G.I[curTn.thumbnailIdx];
5535
5536    
5537      if( G.tn.opt.Get('displayTransition') == 'NONE' ) {
5538        item.$elt.css({ opacity: 1 });
5539        ThumbnailAppearFinish( item );
5540      }
5541      else {
5542        if( item.$elt == null ) { return; }
5543        var top = G.GOM.cache.containerOffset.top + ( curTn.top - G.GOM.clipArea.top );
5544        var vp = G.GOM.cache.viewport;
5545        if( (top + (curTn.top - G.GOM.clipArea.top)) >= (vp.t - 50) && top <= (vp.t + vp.h + 50) ) {
5546          // display animation only if in the current viewport
5547          var delay = cnt * G.tn.opt.Get('displayInterval');
5548          if( G.tn.opt.Get('displayTransition') == 'CUSTOM' ) {
5549            if( G.GOM.curNavLevel == 'lN' ) {
5550              G.O.fnThumbnailDisplayEffect(item.$elt, item, n, delay);
5551            }
5552            else {
5553              G.O.fnThumbnailL1DisplayEffect(item.$elt, item, n, delay);
5554            }
5555          }
5556          else {
5557            G.GOM.thumbnails2Display.push({itm: item, d: delay});
5558            // ThumbnailDisplayAnim2(item, delay);
5559          }
5560          return;
5561        }
5562        else {
5563          item.$elt.css({ opacity: 1 });
5564          ThumbnailAppearFinish(item);
5565        }
5566      }
5567    }
5568    
5569    
5570    // displays thumbnail stacks at the end of the display animation
5571    function ThumbnailAppearFinish( item ) {
5572    
5573      // add stacks
5574      var ns = G.tn.opt.Get('stacks');
5575      if( ns > 0 ) {
5576        // display stacks
5577        item.$elt.css({ display: 'block'});
5578        var o = 0.9;
5579        // set stack opacity
5580        for( var i = ns-1; i>=0; i-- ) {
5581          item.$elt.eq(i).css('opacity', o);
5582          o = o - 0.2;
5583        }
5584
5585      }
5586    }
5587    
5588
5589    function ThumbnailDisplayAnim2( item, delay ) {
5590      function randomIntFromInterval(min,max) {
5591        return Math.floor(Math.random()*(max-min+1)+min);
5592      }
5593      var oFrom = {};
5594      var oTo = {};
5595    
5596      switch (G.tn.opt.Get('displayTransition')) {
5597        case 'RANDOMSCALE': {
5598            var scales = [0.95, 1, 1.05, 1.1];
5599            var zi = [1, 2, 3, 4];
5600            
5601            var r = randomIntFromInterval(0,3);
5602            while( r == G.GOM.lastRandomValue ) {
5603              r = randomIntFromInterval(0,3);
5604            }
5605            G.GOM.lastRandomValue = r;
5606            let f = scales[r];
5607            // item.$elt.css({ 'z-index': G.GOM.lastZIndex+zi[r], 'box-shadow': '-1px 2px 5px 1px rgba(0, 0, 0, 0.7)' });
5608            item.$elt.css({ 'z-index': G.GOM.lastZIndex+zi[r], 'box-shadow': '0px 0px 5px 3px rgba(0,0,0,0.74)' });
5609            
5610            oFrom = { scale: 0.5, opacity:0 };
5611            oTo =   { scale: f,   opacity:1 };
5612            break;
5613          }
5614
5615        case 'SCALEUP': {
5616            let f = G.tn.opt.Get('displayTransitionStartVal');
5617            if( f == 0 ) { f = 0.6; }   // default value
5618            oFrom = { scale: f, opacity: 0 };
5619            oTo =   { scale: 1, opacity: 1 };
5620            break;
5621          }
5622
5623        case 'SCALEDOWN': {
5624            let f = G.tn.opt.Get('displayTransitionStartVal');
5625            if( f == 0 ) { f=1.3; }   // default value
5626            oFrom = { scale: f, opacity: 0 };
5627            oTo =   { scale: 1, opacity: 1 };
5628            break;
5629          }
5630        case 'SLIDEUP': {
5631            let f = G.tn.opt.Get('displayTransitionStartVal');
5632            if( f == 0 ) { f=50; }   // default value
5633            oFrom = { opacity: 0, translateY: f };
5634            oTo =   { opacity: 1, translateY: 0 };
5635            break;
5636          }
5637        case 'SLIDEDOWN': {
5638            let f=G.tn.opt.Get('displayTransitionStartVal');
5639            if( f == 0 ) { f=-50; }   // default value
5640            oFrom = { opacity: 0, translateY: f };
5641            oTo =   { opacity: 1, translateY: 0 };
5642            break;
5643          }
5644        case 'FLIPUP': {
5645            let f=G.tn.opt.Get('displayTransitionStartVal');
5646            if( f == 0 ) { f=100; }   // default value
5647            oFrom = { opacity: 0, translateY: f, rotateX: 45 };
5648            oTo =   { opacity: 1, translateY: 0, rotateX: 0  };
5649            break;
5650          }
5651        case 'FLIPDOWN': {
5652            let f=G.tn.opt.Get('displayTransitionStartVal');
5653            if( f == 0 ) { f=-100; }   // default value
5654            oFrom = { opacity: 0, translateY: f, rotateX: -45 };
5655            oTo =   { opacity: 1, translateY: 0, rotateX: 0 };
5656            break;
5657          }
5658        case 'SLIDEUP2': {
5659            let f = G.tn.opt.Get('displayTransitionStartVal');
5660            if( f == 0 ) { f=100; }   // default value
5661            oFrom = { opacity: 0, translateY: f, rotateY: 40 };
5662            oTo =   { opacity: 1, translateY: 0, rotateY: 0  };
5663            break;
5664          }
5665        case 'IMAGESLIDEUP': {
5666            // let f = G.tn.opt.Get('displayTransitionStartVal');
5667            // if( f == 0 ) { f=100; }   // default value
5668            oFrom = { opacity: 0, top: '100%' };
5669            oTo =   { opacity: 1, top: '0%'  };
5670            break;
5671          }
5672        case 'SLIDEDOWN2': {
5673            let f=G.tn.opt.Get('displayTransitionStartVal');
5674            if( f == 0 ) { f=-100; }   // default value
5675            oFrom = { opacity: 0, translateY: f, rotateY: 40 };
5676            oTo =   { opacity: 1, translateY: 0, rotateY: 0  };
5677            break;
5678          }
5679        case 'SLIDERIGHT': {
5680            let f=G.tn.opt.Get('displayTransitionStartVal');
5681            if( f == 0 ) { f=-150; }   // default value
5682            oFrom = { opacity: 0, translateX: f };
5683            oTo =   { opacity: 1, translateX: 0 };
5684            break;
5685          }
5686        case 'SLIDELEFT': {
5687            let f=G.tn.opt.Get('displayTransitionStartVal');
5688            if( f == 0 ) { f=150; }   // default value
5689            oFrom = { opacity: 0, translateX: f };
5690            oTo =   { opacity: 1, translateX: 0 };
5691            break;
5692          }
5693        case 'FADEIN':
5694          oFrom = { opacity: 0 };
5695          oTo =   { opacity: 1 };
5696          break;
5697          
5698          
5699      }
5700    
5701      var tweenable = new NGTweenable();
5702      tweenable.tween({
5703        from:         oFrom,
5704        to:           oTo,
5705        attachment:   { $e:item.$elt, item: item, tw: tweenable },
5706        delay:        delay,
5707        duration:     G.tn.opt.Get('displayTransitionDuration'),
5708        easing:       G.tn.opt.Get('displayTransitionEasing'),
5709        step:         function (state, att) {
5710          window.requestAnimationFrame( function() {
5711            if( att.item.$elt === null ) {  // the thumbnail may have been destroyed since the start of the animation
5712              att.tw.stop(false);
5713              return;
5714            }
5715            switch (G.tn.opt.Get('displayTransition')) {
5716              case 'RANDOMSCALE':
5717                att.$e.css( G.CSStransformName , 'scale(' + state.scale + ')').css('opacity', state.opacity);
5718                break;
5719              case 'SCALEUP':
5720                att.$e.css( G.CSStransformName , 'scale('+state.scale+')').css('opacity',state.opacity);
5721                break;
5722              case 'SCALEDOWN':
5723                att.item.$elt.last().css('opacity', state.opacity);
5724                att.item.CSSTransformSet('.nGY2GThumbnail', 'scale', state.scale);
5725                att.item.CSSTransformApply('.nGY2GThumbnail');
5726                break;
5727              case 'SLIDEUP':
5728                att.item.$elt.css('opacity', state.opacity);
5729                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', '0px, '+state.translateY + 'px');
5730                att.item.CSSTransformApply('.nGY2GThumbnail');
5731                break;
5732              case 'SLIDEDOWN':
5733                att.item.$elt.css('opacity', state.opacity);
5734                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', '0px,'+state.translateY+'px');
5735                att.item.CSSTransformApply('.nGY2GThumbnail');
5736                break;
5737              case 'FLIPUP':
5738                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', '0px,'+state.translateY+'px');
5739                att.item.CSSTransformSet('.nGY2GThumbnail', 'rotateX', state.rotateX+'deg');
5740                att.item.$elt.css('opacity', state.opacity);
5741                att.item.CSSTransformApply('.nGY2GThumbnail');
5742                break;
5743              case 'FLIPDOWN':
5744                att.item.$elt.css('opacity', state.opacity);
5745                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', '0px,' + state.translateY + 'px');
5746                att.item.CSSTransformSet('.nGY2GThumbnail', 'rotateX', state.rotateX + 'deg');
5747                att.item.CSSTransformApply('.nGY2GThumbnail');
5748                break;
5749              case 'SLIDEUP2':
5750                att.item.$elt.css('opacity', state.opacity);
5751                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', '0px,' + state.translateY + 'px');
5752                att.item.CSSTransformSet('.nGY2GThumbnail', 'rotateY', state.rotateY + 'deg');
5753                att.item.CSSTransformApply('.nGY2GThumbnail');
5754                break;
5755              case 'IMAGESLIDEUP':
5756                att.item.$elt.css('opacity', state.opacity);
5757								att.item.$Elts['.nGY2GThumbnailImage'].css('top', state.top);
5758                break;
5759              case 'SLIDEDOWN2':
5760                att.item.$elt.css('opacity', state.opacity);
5761                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', '0px, ' + state.translateY + 'px');
5762                att.item.CSSTransformSet('.nGY2GThumbnail', 'rotateY', state.rotateY + 'deg');
5763                att.item.CSSTransformApply('.nGY2GThumbnail');
5764                break;
5765              case 'SLIDERIGHT':
5766                att.item.$elt.css('opacity', state.opacity);
5767                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', state.translateX + 'px, 0px');
5768                att.item.CSSTransformApply('.nGY2GThumbnail');
5769                break;
5770              case 'SLIDELEFT':
5771                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', state.translateX + 'px, 0px');
5772                att.item.$elt.css('opacity', state.opacity);
5773                att.item.CSSTransformApply('.nGY2GThumbnail');
5774                break;
5775              case 'FADEIN':
5776                att.$e.css(state);
5777                break;
5778            }
5779          });
5780          // att.$e.css( G.CSStransformName , 'scale('+state.scale+')').css('opacity',state.opacity);
5781        },
5782        finish:       function (state, att) {
5783          window.requestAnimationFrame( function() {
5784            if( att.item.$elt === null ) { return; }
5785            
5786            switch (G.tn.opt.Get('displayTransition')) {
5787              case 'RANDOMSCALE':
5788                att.$e.css( G.CSStransformName , 'scale('+state.scale+')').css('opacity', '');
5789                break;
5790              case 'SCALEUP':
5791                att.$e.css( G.CSStransformName , '').css('opacity', '');
5792                break;
5793              case 'SCALEDOWN':
5794                att.item.$elt.last().css('opacity', '');
5795                att.item.CSSTransformSet('.nGY2GThumbnail', 'scale', state.scale);
5796                att.item.CSSTransformApply('.nGY2GThumbnail');
5797                break;
5798              case 'IMAGESLIDEUP':
5799                att.item.$elt.css('opacity', '');
5800								att.item.$Elts['.nGY2GThumbnailImage'].css('top', 0);
5801                break;
5802              case 'SLIDEDOWN2':
5803                att.item.$elt.css('opacity', '');
5804                att.item.CSSTransformApply('.nGY2GThumbnail');
5805                break;
5806							default :
5807								// case 'SLIDEUP':
5808								// case 'SLIDEDOWN':
5809								// case 'FLIPUP':
5810								// case 'FLIPDOWN':
5811								// case 'SLIDEUP2':
5812								// case 'SLIDERIGHT':
5813								// case 'SLIDELEFT':
5814								// case 'FADEIN':
5815                att.item.$elt.css('opacity', '');
5816            }
5817            ThumbnailAppearFinish(att.item);
5818          });
5819
5820        }
5821      });
5822    
5823    }
5824    
5825    // batch display thumbnails with animation
5826    function ThumbnailDisplayAnimBatch() {
5827      
5828      G.GOM.thumbnails2Display.forEach( function(one) {
5829        ThumbnailDisplayAnim2(one.itm, one.d);
5830      });
5831      G.GOM.thumbnails2Display=[];
5832    }
5833    
5834    
5835
5836    // ######################################
5837    // Gallery display animation
5838    function GalleryAppear() {
5839      
5840      var d=G.galleryDisplayTransitionDuration.Get();
5841      switch( G.galleryDisplayTransition.Get() ){
5842        case 'ROTATEX':
5843          G.$E.base.css({ perspective: '1000px', 'perspective-origin': '50% 0%' });
5844          new NGTweenable().tween({
5845            from:         { r: 50 },
5846            to:           { r: 0  },
5847            attachment:   { orgIdx: G.GOM.albumIdx },
5848            duration:     d,
5849            easing:       'easeOutCirc',
5850            step:         function (state, att) {
5851              if( att.orgIdx == G.GOM.albumIdx ) {
5852                // window.ng_draf( function() {
5853                  G.$E.conTnParent.css( G.CSStransformName , 'rotateX(' + state.r + 'deg)');
5854                // });
5855              }
5856            }
5857          });
5858          break;
5859        case 'SLIDEUP':
5860          G.$E.conTnParent.css({ opacity: 0 });
5861          new NGTweenable().tween({
5862            from:         { y: 200, o: 0 },
5863            to:           { y: 0,   o: 1 },
5864            attachment:   { orgIdx: G.GOM.albumIdx },
5865            duration:     d,
5866            easing:       'easeOutCirc',
5867            step:         function (state, att) {
5868              if( att.orgIdx == G.GOM.albumIdx ) {
5869                // window.ng_draf( function() {
5870                  G.$E.conTnParent.css( G.CSStransformName , 'translate( 0px, '+state.y + 'px)').css('opacity', state.o);
5871                // });
5872              }
5873            }
5874          });
5875          break;
5876        case 'NONE':
5877        default:
5878          break;
5879      }
5880
5881
5882    }
5883    
5884    // ######################################
5885    // ##### THUMBNAIL HOVER MANAGEMENT #####
5886    // ######################################
5887
5888    function ThumbnailOverInit( GOMidx ) {
5889      // Over init in 2 step:
5890      // 1) init with thumbnailBuildInit2 parameter
5891      // 2) init with the hover effect parameter
5892      
5893      
5894      var curTn = G.GOM.items[GOMidx];
5895      var item = G.I[curTn.thumbnailIdx];
5896
5897      if( item.$elt == null ) { return; } // zombie
5898      
5899      var fu = G.O.fnThumbnailHoverInit;
5900      if( fu !== null ) {
5901        typeof fu == 'function' ? fu($e, item, GOMidx) : window[fu]($e, item, GOMidx);
5902      }
5903
5904      // build initialization
5905      var inits = G.tn.buildInit.get();
5906      for( var j = 0; j < inits.length; j++) {
5907        switch( inits[j].property ) {
5908          // CSS Transform
5909          case 'scale':
5910          case 'rotateX':
5911          case 'rotateY':
5912          case 'rotateZ':
5913          case 'translateX':
5914          case 'translateY':
5915          case 'translateZ':
5916            item.CSSTransformSet(inits[j].element, inits[j].property, inits[j].value);
5917            item.CSSTransformApply(inits[j].element);
5918            break;
5919          // CSS filter
5920          case 'blur':
5921          case 'brightness':
5922          case 'grayscale':
5923          case 'sepia':
5924          case 'contrast':
5925          case 'opacity':
5926          case 'saturate':
5927            item.CSSFilterSet(inits[j].element, inits[j].property, inits[j].value);
5928            item.CSSFilterApply(inits[j].element);
5929            break;
5930          default:
5931            var $t=item.$getElt(inits[j].element);
5932            $t.css( inits[j].property, inits[j].value );
5933            break;
5934        }
5935      }
5936      
5937      // hover
5938      var effects = G.tn.hoverEffects.get();
5939      for( var j = 0; j < effects.length; j++) {
5940        if( effects[j].firstKeyframe === true ) {
5941          switch( effects[j].type ) {
5942            case 'scale':
5943            case 'rotateX':
5944            case 'rotateY':
5945            case 'rotateZ':
5946            case 'translateX':
5947            case 'translateY':
5948            case 'translateZ':
5949              item.CSSTransformSet(effects[j].element, effects[j].type, effects[j].from);
5950              item.CSSTransformApply(effects[j].element);
5951              break;
5952            case 'blur':
5953            case 'brightness':
5954            case 'grayscale':
5955            case 'sepia':
5956            case 'contrast':
5957            case 'opacity':
5958            case 'saturate':
5959              item.CSSFilterSet(effects[j].element, effects[j].type, effects[j].from);
5960              item.CSSFilterApply(effects[j].element);
5961              break;
5962            default:
5963              var $t = item.$getElt(effects[j].element);
5964              $t.css( effects[j].type, effects[j].from );
5965              break;
5966              
5967          }
5968        }
5969      }
5970      item.hoverInitDone=true;
5971    }
5972
5973    function ThumbnailHoverReInitAll() {
5974      if( G.GOM.albumIdx == -1 ) { return; };
5975      var l = G.GOM.items.length;
5976      for( var i = 0; i < l ; i++ ) {
5977        ThumbnailOverInit(i);
5978        // G.GOM.items[i].hovered=false;
5979        G.I[G.GOM.items[i].thumbnailIdx].hovered = false;
5980      }
5981    }
5982
5983
5984    function ThumbnailHover( GOMidx ) {
5985      if( G.GOM.albumIdx == -1 || !G.galleryResizeEventEnabled ) { return; };
5986      if( G.GOM.slider.hostIdx == GOMidx ) {
5987        // slider hosted on thumbnail -> no hover effect
5988        return;
5989      }
5990      var curTn = G.GOM.items[GOMidx];
5991      var item = G.I[curTn.thumbnailIdx];
5992      if( item.kind == 'albumUp' || item.$elt == null ) { return; }
5993
5994      item.hovered = true;
5995
5996      var fu = G.O.fnThumbnailHover;
5997      if( fu !== null ) {
5998        typeof fu == 'function' ? fu(item.$elt, item, GOMidx) : window[fu](item.$elt, item, GOMidx);
5999      }
6000      var effects = G.tn.hoverEffects.get();
6001
6002      try {
6003        for( var j = 0; j < effects.length; j++) {
6004          if( effects[j].hoverin === true ) {
6005            //item.animate( effects[j], j*10,  true );
6006            item.animate( effects[j], 0,  true );
6007          }
6008        }
6009        // effects on whole layout
6010        // GalleryResize( GOMidx );
6011      }
6012      catch (e) { 
6013        NanoAlert(G, 'error on hover: ' + e.message );
6014      }
6015
6016    }
6017
6018    function ThumbnailHoverOutAll() {
6019      if( G.GOM.albumIdx == -1 ) { return; };
6020      var l = G.GOM.items.length;
6021      for( var i = 0; i < l ; i++ ) {
6022        if( G.GOM.items[i].inDisplayArea ) {
6023          ThumbnailHoverOut(i);
6024        }
6025        else {
6026          G.I[G.GOM.items[i].thumbnailIdx].hovered = false;
6027        }
6028      }
6029    }
6030
6031    
6032    function ThumbnailHoverOut( GOMidx ) {
6033      if( G.GOM.albumIdx == -1 || !G.galleryResizeEventEnabled ) { return; }
6034
6035      if( G.GOM.slider.hostIdx == GOMidx ) {
6036        // slider on thumbnail -> no hover effect
6037        return;
6038      }
6039
6040      var curTn = G.GOM.items[GOMidx];
6041      var item = G.I[curTn.thumbnailIdx];
6042      if( item.kind == 'albumUp' || !item.hovered ) { return; }
6043      item.hovered = false;
6044      if( item.$elt == null ) { return; }
6045
6046      var fu = G.O.fnThumbnailHoverOut;
6047      if( fu !== null ) {
6048        typeof fu == 'function' ? fu(item.$elt, item, GOMidx) : window[fu](item.$elt, item, GOMidx);
6049      }
6050
6051      var effects = G.tn.hoverEffects.get();
6052      try {
6053        for( var j = 0; j < effects.length; j++) {
6054          if( effects[j].hoverout === true ) {
6055            // item.animate( effects[j], j*10, false );
6056            item.animate( effects[j], 0, false );
6057          }
6058        }
6059        // effects on whole layout
6060        // GalleryResize( );
6061      }
6062      catch (e) { 
6063        NanoAlert(G, 'error on hoverOut: ' + e.message );
6064      }
6065      
6066    }
6067    
6068
6069    /** @function DisplayPhoto */
6070    function DisplayPhoto( imageID, albumID ) {
6071
6072      if( G.O.debugMode ) { console.log('#DisplayPhoto : '+  albumID +'-'+ imageID); }
6073      var albumIdx = NGY2Item.GetIdx(G, albumID);
6074      if( albumIdx == 0 ) {
6075        G.GOM.curNavLevel = 'l1';
6076      }
6077      else {
6078        G.GOM.curNavLevel = 'lN';
6079      }
6080
6081      if( albumIdx == -1 ) {
6082        // get content of album on root level
6083        if( G.O.kind != '' ) {
6084          // do not add album if Markup or Javascript data
6085          NGY2Item.New( G, '', '', albumID, '0', 'album' );    // create empty album
6086          // albumIdx = G.I.length - 1;
6087        }
6088      }
6089
6090      var ngy2ItemIdx = NGY2Item.GetIdx(G, imageID);
6091      if( ngy2ItemIdx == -1 ) {
6092        // get content of the album
6093        AlbumGetContent( albumID, DisplayPhoto, imageID, albumID );
6094        return;
6095      }
6096      
6097      if( G.O.debugMode ) { console.log('#DisplayPhoto : '+  ngy2ItemIdx); }
6098     
6099      DisplayPhotoIdx(ngy2ItemIdx);
6100    
6101    }
6102   
6103    // function AlbumGetContent( albumIdx, fnToCall ) {
6104    function AlbumGetContent( albumID, fnToCall, fnParam1, fnParam2 ) {
6105      // var url='';
6106      // var kind='image';
6107      // var albumIdx=NGY2Item.GetIdx(G, albumID);
6108      // var photoIdx=NGY2Item.GetIdx(G, photoID);
6109      
6110      switch( G.O.kind ) {
6111        // MARKUP / API
6112        case '':
6113          AlbumGetMarkupOrApi(fnToCall, fnParam1, fnParam2);
6114          break;
6115        // JSON, Flickr, Picasa, ...
6116        default:
6117          jQuery.nanogallery2['data_'+G.O.kind](G, 'AlbumGetContent', albumID, fnToCall, fnParam1, fnParam2 );
6118      }
6119      
6120    }
6121    
6122    var mediaList = {
6123      youtube : {
6124        getID: function( url ) {
6125          // https://stackoverflow.com/questions/10591547/how-to-get-youtube-video-id-from-url
6126          var s = url.match( /(?:https?:\/{2})?(?:w{3}\.)?youtu(?:be)?\.(?:com|be)(?:\/watch\?v=|\/)([^\s&]+)/ );
6127          return s != null ? s[1] : null;
6128        },
6129        thumbUrl: function( id ) {
6130          return 'https://img.youtube.com/vi/' + id + '/hqdefault.jpg';
6131        },
6132        url: function( id ) {
6133          return 'https://www.youtube.com/embed/' + id;
6134        },
6135        markup: function( id ) {
6136          // return '<iframe class="nGY2ViewerMedia" src="https://www.youtube.com/embed/' + id + '?rel=0" frameborder="0" gesture="media" allowfullscreen></iframe>';
6137          return '<iframe class="nGY2ViewerMedia" src="https://www.youtube.com/embed/' + id + '?rel=0" frameborder="0" allow="autoplay" allowfullscreen></iframe>';
6138        },
6139        kind: 'iframe'
6140      },
6141      vimeo : {
6142        getID: function( url ) {
6143          // https://stackoverflow.com/questions/2916544/parsing-a-vimeo-id-using-javascript
6144          // var s = url.match( /^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/ );
6145          var s = url.match( /(http|https)?:\/\/(www\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|)(\d+)(?:|\/\?)/ );
6146          return s != null ? s[4] : null;
6147        },
6148        url: function( id ) {
6149          return 'https://player.vimeo.com/video/' + id;
6150        },
6151        markup: function( id ) {
6152          // return '<iframe class="nGY2ViewerMedia" src="https://player.vimeo.com/video/' + id + '?rel=0" frameborder="0" gesture="media" allowfullscreen></iframe>';
6153          // return '<iframe class="nGY2ViewerMedia" src="https://player.vimeo.com/video/' + id + '?rel=0" frameborder="0" allow="autoplay; fullscreen" allowfullscreen></iframe>';
6154          return '<iframe class="nGY2ViewerMedia" src="https://player.vimeo.com/video/' + id + '" frameborder="0" allow="autoplay; fullscreen" allowfullscreen></iframe>';
6155        },
6156        kind: 'iframe'
6157      },
6158      dailymotion : {
6159        getID: function( url ) {
6160          // https://stackoverflow.com/questions/12387389/how-to-parse-dailymotion-video-url-in-javascript
6161          var m = url.match(/^.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/);
6162          if (m !== null) {
6163              if(m[4] !== undefined) {
6164                  return m[4];
6165              }
6166              return m[2];
6167          }
6168          return null;
6169        },
6170        thumbUrl: function( id ) {
6171          return 'https://www.dailymotion.com/thumbnail/video/' + id;
6172        },
6173        url: function( id ) {
6174          return 'https://www.dailymotion.com/embed/video/' + id;
6175        },
6176        markup: function( id ) {
6177          // return '<iframe class="nGY2ViewerMedia" src="https://www.dailymotion.com/embed/video/' + id + '?rel=0" frameborder="0" gesture="media" allowfullscreen></iframe>';
6178          return '<iframe class="nGY2ViewerMedia" src="https://www.dailymotion.com/embed/video/' + id + '?rel=0" frameborder="0" allow="autoplay" allowfullscreen></iframe>';
6179        },
6180        kind: 'iframe'
6181      },
6182      selfhosted : {
6183				// SELF-HOSTED VIDEOS
6184        getID: function( url ) {
6185          // In order to leave things as is, I used ID to identify the extension
6186          // https://stackoverflow.com/questions/6997262/how-to-pull-url-file-extension-out-of-url-string-using-javascript
6187          // Make sure the method used for verifying the extension matches the kind of url your selfhosted video has
6188          var extension = url.split('.').pop().toLowerCase();
6189
6190          // supported extensions
6191          var s = ( extension === 'mp4' || extension === 'webm' || extension === 'ogv' || extension === '3gp' ) ? extension : null ;
6192          return s;
6193        },
6194        markup: function( url ) {
6195          // return '<video controls class="nGY2ViewerMedia"><source src="${id.src}" type="video/${id.type}" preload="auto">Your browser does not support the video tag (HTML 5).</video>';
6196          var extension = url.split('.').pop();
6197          return '<video controls class="nGY2ViewerMedia"><source src="'+ url +'" type="video/'+ extension +'" preload="auto">Your browser does not support the video tag (HTML 5).</video>';
6198        },
6199        kind: 'video',
6200        selfhosted : true
6201      }
6202    };
6203        
6204    function AlbumGetMarkupOrApi ( fnToCall, fnParam1, fnParam2 ) {
6205    
6206      if( G.markupOrApiProcessed === true ) {
6207        // already processed (maybe location hash to unknow reference) -> display root album
6208        DisplayAlbum('-1', 0);
6209        return;
6210      }
6211      
6212      if( G.O.items !== undefined && G.O.items !== null ) {
6213        // data defined as an object in an option parameter
6214        GetContentApiObject();
6215      }
6216      else {
6217				if( G.O.$markup.length > 0 ) {
6218          // data defined as markup (href elements)
6219          GetContentMarkup( G.O.$markup );
6220          G.O.$markup=[]  ;
6221        }
6222        else {
6223          NanoConsoleLog(G, 'error: no media to process.');
6224          return;
6225        }
6226      }
6227      
6228      G.markupOrApiProcessed = true;
6229      if( fnToCall !== null &&  fnToCall !== undefined) {
6230        fnToCall( fnParam1, fnParam2, null );
6231      }
6232    }
6233    
6234    function StartsWithProtocol ( path ) {
6235      if( path == undefined ) { return false; }
6236      // if( path == null ) { return false; }
6237      
6238      var pattern = /^((http|https|ftp|ftps|file):\/\/)/;
6239      if( !pattern.test(path) ) {
6240        // not a full URL
6241        return false;
6242      }
6243      return true;
6244    }
6245    
6246    function GetContentApiObject() {
6247      var foundAlbumID=false;
6248      var nbTitles = 0;
6249      var AlbumPostProcess = NGY2Tools.AlbumPostProcess.bind(G);
6250
6251      G.I[0].contentIsLoaded = true;
6252
6253      jQuery.each(G.O.items, function(i,item){
6254        
6255        var title = '';
6256        title = GetI18nItem(item, 'title');
6257        if( title === undefined ) { title=''; }
6258        
6259        var src='';
6260        if( item['src'+RetrieveCurWidth().toUpperCase()] !== undefined ) {
6261          src = item['src'+RetrieveCurWidth().toUpperCase()];
6262        }
6263        else {
6264          src = item.src;
6265        }
6266        if( !StartsWithProtocol(src) ) {
6267          src = G.O.itemsBaseURL + src;
6268        }
6269
6270        var thumbsrc = '';
6271        if( item.srct !== undefined && item.srct.length > 0 ) {
6272          thumbsrc = item.srct;
6273          if( !StartsWithProtocol(thumbsrc) ) {
6274            thumbsrc = G.O.itemsBaseURL + thumbsrc;
6275          }
6276        }
6277        else {
6278          thumbsrc = src;
6279        }
6280        
6281        if( G.O.thumbnailLabel.get('title') != '' ) {
6282          title = GetImageTitle(src);
6283        }
6284
6285        var description='';     //'&nbsp;';
6286        description=GetI18nItem(item,'description');
6287        if( description === undefined ) { description=''; }
6288        //if( toType(item.description) == 'string' ) {
6289        //  description=item.description;
6290        //}
6291
6292        var tags = GetI18nItem(item, 'tags');
6293        if( tags === undefined ) { tags=''; }
6294
6295        var albumID = 0;
6296        if( item.albumID !== undefined  ) {
6297          albumID=item.albumID;
6298          foundAlbumID = true;
6299        }
6300        var ID = null;
6301        if( item.ID !== undefined ) {
6302          ID = item.ID;
6303        }
6304        var kind = 'image';
6305        if( item.kind !== undefined && item.kind.length > 0 ) {
6306          kind = item.kind;
6307        }
6308        
6309        var newItem=NGY2Item.New( G, title, description, ID, albumID, kind, tags );
6310        if( title != '' ) {
6311          nbTitles++;
6312        }
6313        
6314        // media source url - img is the default media kind
6315        newItem.setMediaURL( src, 'img');
6316
6317        // manage media kinds other than IMG
6318        jQuery.each(mediaList, function ( n, media ) {
6319          var id = media.getID(src);
6320          if( id != null ) {
6321            if( thumbsrc == src && typeof media.thumbUrl == 'function' ) {
6322              thumbsrc = media.thumbUrl(id);  
6323            }
6324            if( typeof media.url == 'function' ) { src = media.url(id);  }
6325            newItem.mediaKind = media.kind;
6326            newItem.mediaMarkup = ( media.selfhosted ) ? media.markup( src ) : media.markup(id);
6327            return false;
6328          }
6329        });
6330
6331        // image size
6332        if( item.imageWidth !== undefined ) { newItem.imageWidth = item.width; }
6333        if( item.imageHeight !== undefined ) { newItem.imageHeight = item.height; }
6334
6335        // THUMBNAILS
6336        
6337        // thumbnail image size
6338        var tw = item.imgtWidth !== undefined ? item.imgtWidth : 0;
6339        var th = item.imgtHeight !== undefined ? item.imgtHeight : 0;
6340
6341        // default thumbnail URL and size
6342        newItem.thumbs = {
6343          url:    { l1 : { xs: thumbsrc, sm: thumbsrc, me: thumbsrc, la: thumbsrc, xl: thumbsrc }, lN : { xs: thumbsrc, sm: thumbsrc, me: thumbsrc, la: thumbsrc, xl: thumbsrc } },
6344          width:  { l1 : { xs: tw, sm: tw, me: tw, la: tw, xl: tw }, lN : { xs: tw, sm: tw, me: tw, la: tw, xl: tw } },
6345          height: { l1 : { xs: th, sm: th, me: th, la: th, xl: th }, lN : { xs: th, sm: th, me: th, la: th, xl: th } }
6346        };
6347        
6348        // default media type -> IMG
6349        if( newItem.mediaKind == 'img' ) {
6350        
6351          // responsive thumbnails URL and size
6352          var lst=['xs', 'sm', 'me', 'la', 'xl'];
6353          for( var i=0; i< lst.length; i++ ) {
6354            // url
6355            var turl = item['srct' + lst[i].toUpperCase()];
6356            if( turl !== undefined ) {
6357              if( !StartsWithProtocol(turl) ) {
6358                turl = G.O.itemsBaseURL + turl;
6359              }
6360              newItem.url.l1[lst[i]] = turl;
6361              newItem.url.lN[lst[i]] = turl;
6362            }
6363            // width
6364            var tw = item['imgt' + lst[i].toUpperCase() + 'Width'];
6365            if( tw != undefined ) {
6366              newItem.width.l1[lst[i]] = parseInt(tw);
6367              newItem.width.lN[lst[i]] = parseInt(tw);
6368            }
6369            // height
6370            var th = item['imgt' + lst[i].toUpperCase() + 'Height'];
6371            if( th != undefined ) {
6372              newItem.height.l1[lst[i]] = parseInt(th);
6373              newItem.height.lN[lst[i]] = parseInt(th);
6374            }
6375          }
6376        }
6377        
6378        // dominant colors (needs to be a base64 gif)
6379        if( item.imageDominantColors !== undefined ) {
6380          newItem.imageDominantColors = item.imageDominantColors;
6381        }
6382        // dominant color (rgb hex)
6383        if( item.imageDominantColor !== undefined ) {
6384          newItem.imageDominantColor = item.imageDominantColor;
6385        }
6386        
6387        // dest url
6388        if( item.destURL !== undefined && item.destURL.length>0 ) {
6389          newItem.destinationURL = item.destURL;
6390        }
6391        
6392        // download image url
6393        if( item.downloadURL !== undefined && item.downloadURL.length>0 ) {
6394          newItem.downloadURL = item.downloadURL;
6395        }
6396        
6397        // EXIF DATA
6398        // Exif - model
6399        if( item.exifModel !== undefined ) { newItem.exif.model = item.exifModel; }
6400        // Exif - flash
6401        if( item.exifFlash !== undefined ) { newItem.exif.flash = item.exifFlash; }
6402        // Exif - focallength
6403        if( item.exifFocalLength !== undefined ) { newItem.exif.focallength = item.exifFocalLength; }
6404        // Exif - fstop
6405        if( item.exifFStop !== undefined ) { newItem.exif.fstop = item.exifFStop; }
6406        // Exif - exposure
6407        if( item.exifExposure !== undefined ) { newItem.exif.exposure = item.exifExposure; }
6408        // Exif - time
6409        if( item.exifIso !== undefined ) { newItem.exif.iso = item.exifIso; }
6410        // Exif - iso
6411        if( item.exifTime !== undefined ) { newItem.exif.time = item.exifTime; }
6412        // Exif - location
6413        if( item.exifLocation !== undefined ) { newItem.exif.location  = item.exifLocation; }
6414        
6415        
6416        // custom data
6417        if( item.customData !== null ) {
6418          newItem.customData = cloneJSObject( item.customData );
6419        }
6420
6421        newItem.contentIsLoaded = true;
6422        
6423        var fu = G.O.fnProcessData;
6424        if( fu !== null ) {
6425          typeof fu == 'function' ? fu(newItem, 'api', item) : window[fu](newItem, 'api', item);
6426        }
6427        
6428        AlbumPostProcess(albumID);
6429      });
6430      
6431      // if( foundAlbumID ) { G.O.displayBreadcrumb=true; }
6432      if( nbTitles == 0 ) { G.O.thumbnailLabel.display=false; }
6433
6434    }
6435    
6436
6437    // Returns the text of the DOM element (without children)
6438    // Because jQuery().text() returns the text of all children
6439    function ElementGetText( element ) {
6440      
6441      var text = '';
6442      if( element.childNodes[0] !== undefined ) {
6443        if( element.childNodes[0].nodeValue !== null && element.childNodes[0].nodeValue !== undefined ) {
6444          text = element.childNodes[0].nodeValue.trim();
6445        }
6446      }
6447      return text;
6448    }
6449    
6450    // Extract items from the jQuery elements
6451    function GetContentMarkup( $elements, group ) {
6452      var foundAlbumID = false;
6453      var nbTitles = 0;
6454      var AlbumPostProcess = NGY2Tools.AlbumPostProcess.bind(G);
6455      var GetImageTitleFromURL = NGY2Tools.GetImageTitleFromURL.bind(G);
6456
6457      G.I[0].contentIsLoaded = true;
6458
6459      jQuery.each($elements, function(i, item){
6460
6461        // compare to group defined on the element that has been clicked (lightbox standalone)
6462        if( item.dataset.nanogallery2Lgroup != group ) { return; }
6463
6464        // ignore element <SCRIPT>
6465        if( item.nodeName == 'SCRIPT' ) { return; }
6466
6467        // create dictionnary with all data attribute name in lowercase (to be case unsensitive)
6468        var data = {
6469          // all possible data attributes with some default values
6470          'data-ngdesc':                  '',         // item description
6471          'data-ngid':                    null,       // ID
6472          'data-ngkind':                  'image',    // kind (image, album, albumup)
6473          'data-ngtags':                  null,       // tags
6474          'data-ngdest':                  '',         // destination URL
6475          'data-ngthumbimgwidth':         0,          // thumbnail width
6476          'data-ngthumbimgheight':        0,          // thumbnail height
6477          'data-ngimagewidth':            0,          // image width
6478          'data-ngimageheight':           0,          // image height
6479          'data-ngimagedominantcolors':   null,       // image dominant colors
6480          'data-ngimagedominantcolor':    null,       // image dominant colors
6481          'data-ngexifmodel':             '',         // EXIF data
6482          'data-ngexifflash':             '',
6483          'data-ngexiffocallength':       '',
6484          'data-ngexiffstop':             '',
6485          'data-ngexifexposure':          '',
6486          'data-ngexifiso':               '',
6487          'data-ngexiftime':              '',
6488          'data-ngexiflocation':          '',
6489          'data-ngsrc':    					      '',
6490					'alt':													''
6491        };
6492
6493        // Extract data attributes from main item
6494        [].forEach.call( item.attributes, function(attr) {
6495          data[attr.name.toLowerCase()] = attr.value.trim();
6496        });
6497
6498        var title = ElementGetText(item);
6499				if( title == '' && data.alt != '') {
6500					// no title -> check ALT attribute of main element
6501					title = data['alt'];
6502				}
6503        
6504        // Complete with data attributes from all children
6505        jQuery.each($(item).children(), function(i, sub_item){
6506          
6507          // title may be set on a child element
6508          if( title == '' ) {
6509            title = ElementGetText(sub_item);
6510          }
6511          
6512          [].forEach.call( sub_item.attributes, function(attr) {
6513            data[attr.name.toLowerCase()] = attr.value.trim();
6514          });
6515
6516					if( title == '' && data.alt != '') {
6517						// no title -> check ALT attribute of sub element
6518						title = data['alt'];
6519					}
6520
6521				});
6522
6523				// BIG IMAGE URL
6524        // responsive image URL
6525        var src = '',
6526        st = RetrieveCurWidth().toUpperCase();
6527        if( data.hasOwnProperty('data-ngsrc'+st) ) {
6528          src = data['data-ngsrc'+st];
6529        }
6530				// image URL from data-ngsrc attribute 
6531        // if( src == '' ) {
6532          // src = data['data-ngsrc'];
6533        // }
6534				// image URL from href attribute (a element)
6535        // if( src == '' ) {
6536          // src = data['href'];
6537        // }
6538        src = src || data['data-ngsrc'] || data['href'];
6539        if( src !== undefined && !StartsWithProtocol(src) ) {          // do not add the base URL if src starts with a protocol (http, https...)
6540          src = G.O.itemsBaseURL + src;
6541        }
6542
6543        // THUMBNAIL IMAGE
6544        var thumbsrc = '';
6545				// src attribute (img element)
6546        if( data.hasOwnProperty('src') ) {
6547          thumbsrc = data['src'];
6548        }
6549				// data-ngthumb attribute
6550				if( thumbsrc == '' && data.hasOwnProperty('data-ngthumb') ) {
6551          thumbsrc = data['data-ngthumb'];
6552        }
6553        if( thumbsrc == '' ) {
6554          thumbsrc = src;       // no thumbnail image URL -> use big image URL
6555        }
6556				if( thumbsrc !== undefined && !StartsWithProtocol(thumbsrc) ) {
6557					thumbsrc = G.O.itemsBaseURL + thumbsrc;
6558				}
6559
6560        // ignore if no media URL at all
6561        if( src === undefined && thumbsrc === undefined ) { return; }
6562        
6563        //newObj.description=jQuery(item).attr('data-ngdesc');
6564        var description = data['data-ngdesc'];
6565        var ID = data['id'] || data['data-ngid'];
6566        // if( ID == undefined ) {
6567          // ID = data['data-ngid'];
6568        // }
6569        var kind = data['data-ngkind'];
6570        var tags = data['data-ngtags'];
6571
6572        var albumID = '0';
6573        if( data.hasOwnProperty('data-ngalbumid') ) {
6574          albumID = data['data-ngalbumid'];
6575          foundAlbumID = true;
6576        }
6577
6578        // var title = jQuery(item).text();
6579        var title_from_url = GetImageTitleFromURL( src );
6580        if( title_from_url != '' ) {
6581          title = title_from_url;
6582        }
6583
6584        var newItem = NGY2Item.New( G, title, description, ID, albumID, kind, tags );
6585        if( title != '' ) {
6586          nbTitles++;
6587        }
6588
6589        // media source url - img is the default media kind
6590        newItem.setMediaURL( src, 'img');
6591
6592        // manage media kinds other than IMG
6593        // newItem.mediaKind = 'img';
6594        jQuery.each(mediaList, function ( n, media ) {
6595          var id = media.getID(src);
6596          if( id != null ) {
6597            if( thumbsrc == src && typeof media.thumbUrl == 'function' ) {
6598              thumbsrc = media.thumbUrl(id);  
6599            }
6600            if( typeof media.url == 'function' ) { src = media.url(id);  }
6601            newItem.mediaKind = media.kind;
6602            newItem.mediaMarkup = ( media.selfhosted ) ? media.markup( src ) : media.markup(id);
6603            return false;
6604          }
6605        });
6606        
6607        
6608        // Big image size
6609        newItem.imageWidth = parseInt( data['data-ngimagewidth'] );
6610        newItem.imageHeight = parseInt( data['data-ngimageheight'] );
6611        
6612        // default thumbnail image URL and size
6613        var tw = parseInt(data['data-ngthumbimgwidth']);
6614        var th = parseInt(data['data-ngthumbimgheight']);
6615        newItem.thumbs = {
6616          url:    { l1 : { xs: thumbsrc, sm: thumbsrc, me: thumbsrc, la: thumbsrc, xl: thumbsrc }, lN : { xs: thumbsrc, sm: thumbsrc, me: thumbsrc, la: thumbsrc, xl: thumbsrc } },
6617          width:  { l1 : { xs: tw, sm: tw, me: tw, la: tw, xl: tw }, lN : { xs: tw, sm: tw, me: tw, la: tw, xl: tw } },
6618          height: { l1 : { xs: th, sm: th, me: th, la: th, xl: th }, lN : { xs: th, sm: th, me: th, la: th, xl: th } }
6619        };
6620
6621        // Media type -> IMG
6622        if( newItem.mediaKind == 'img' ) {
6623        
6624          // responsive thumbnails URL and size
6625          var lst = ['xs', 'sm', 'me', 'la', 'xl'];
6626          for( var i = 0; i < lst.length; i++ ) {
6627            // url
6628            if( data.hasOwnProperty('data-ngthumb' + lst[i]) ) {
6629              var turl=data['data-ngthumb' + lst[i]];
6630              if( !StartsWithProtocol(turl) ) {
6631                turl = G.O.itemsBaseURL + turl;
6632              }
6633              newItem.url.l1[lst[i]] = turl;
6634              newItem.url.lN[lst[i]] = turl;
6635            }
6636
6637            // width
6638            if( data.hasOwnProperty('data-ngthumb' + lst[i] + 'width') ) {
6639              var tw=parseInt(data['data-ngthumb' + lst[i] + 'width']);
6640              newItem.width.l1[lst[i]] = tw;
6641              newItem.width.lN[lst[i]] = tw;
6642            }
6643            // height
6644            if( data.hasOwnProperty('data-ngthumb' + lst[i] + 'height') ) {
6645              var th=parseInt('data-ngthumb' + lst[i] + 'height');
6646              newItem.height.l1[lst[i]] = th;
6647              newItem.height.lN[lst[i]] = th;
6648            }
6649          }
6650        }
6651        
6652        
6653        // dominant colorS (needs to be a base64 gif)
6654        newItem.imageDominantColors = data['data-ngimagedominantcolors'];
6655        // dominant color (rgb hex)
6656        newItem.imageDominantColor = data['data-ngimagedominantcolors'];
6657
6658        newItem.destinationURL = data['data-ngdest'];
6659        newItem.downloadURL = data['data-ngdownloadurl'];
6660
6661        // Exif - model
6662        newItem.exif.model=data['data-ngexifmodel'];
6663        // Exif - flash
6664        newItem.exif.flash=data['data-ngexifflash'];
6665        // Exif - focallength
6666        newItem.exif.focallength=data['data-ngexiffocallength'];
6667        // Exif - fstop
6668        newItem.exif.fstop=data['data-ngexiffstop'];
6669        // Exif - exposure
6670        newItem.exif.exposure=data['data-ngexifexposure'];
6671        // Exif - iso
6672        newItem.exif.iso=data['data-ngexifiso'];
6673        // Exif - time
6674        newItem.exif.time=data['data-ngexiftime'];
6675        // Exif - location
6676        newItem.exif.location=data['data-ngexiflocation'];
6677        
6678        newItem.contentIsLoaded = true;
6679
6680        // custom data
6681        if( jQuery(item).data('customdata') !== undefined ) {
6682          newItem.customData = cloneJSObject(jQuery(item).data('customdata'));
6683        }
6684        // custom data
6685        if( jQuery(item).data('ngcustomdata') !== undefined ) {
6686          newItem.customData = cloneJSObject(jQuery(item).data('ngcustomdata'));
6687        }
6688
6689        var fu=G.O.fnProcessData;
6690        if( fu !== null ) {
6691          typeof fu == 'function' ? fu(newItem, 'markup', item) : window[fu](newItem, 'markup', item);
6692        }
6693        
6694        AlbumPostProcess(albumID);
6695
6696      });
6697      
6698      // if( foundAlbumID ) { G.O.displayBreadcrumb=true; }
6699      if( nbTitles == 0 ) { G.O.thumbnailLabel.display = false; }
6700			
6701    }
6702
6703    
6704    // ################################
6705    // ##### DEFINE VARIABLES     #####
6706    // ################################
6707
6708    
6709    /** @function DefineVariables */
6710    function DefineVariables() {
6711    
6712      // change 'picasa' to 'google' for compatibility reason
6713      if( G.O.kind.toUpperCase() == 'PICASA'  || G.O.kind.toUpperCase() == 'GOOGLE') {
6714        G.O.kind='google2';
6715      }
6716    
6717      // management of screen width
6718      G.GOM.cache.viewport = getViewport();
6719      G.GOM.curWidth = RetrieveCurWidth();
6720      
6721      // tumbnail toolbar
6722      jQuery.extend(true, G.tn.toolbar.image, G.O.thumbnailToolbarImage );
6723      jQuery.extend(true, G.tn.toolbar.album, G.O.thumbnailToolbarAlbum );
6724      var t = ['image', 'album'];
6725      var pos= ['topLeft', 'topRight', 'bottomLeft', 'bottomRight']
6726      for( var i=0; i < t.length ; i++ ) {
6727        for( var j=0; j < pos.length ; j++ ) {
6728          G.tn.toolbar[t[i]][pos[j]] = G.tn.toolbar[t[i]][pos[j]].toUpperCase();
6729        }
6730      }
6731
6732      // convert label settings
6733      if( G.O.thumbnailLabel.position == 'overImageOnBottom' ) {
6734        G.O.thumbnailLabel.valign = 'bottom';
6735        G.O.thumbnailLabel.position = 'overImage';
6736      }
6737      if( G.O.thumbnailLabel.position == 'overImageOnMiddle' ) {
6738        G.O.thumbnailLabel.valign = 'middle';
6739        G.O.thumbnailLabel.position = 'overImage';
6740      }
6741      if( G.O.thumbnailLabel.position == 'overImageOnTop' ) {
6742        G.O.thumbnailLabel.valign = 'top';
6743        G.O.thumbnailLabel.position = 'overImage';
6744      }
6745      if( G.O.thumbnailL1Label !== undefined && G.O.thumbnailL1Label.position !== undefined ) {
6746        if( G.O.thumbnailL1Label.position == 'overImageOnBottom' ) {
6747          G.O.thumbnailL1Label.valign = 'bottom';
6748          G.O.thumbnailL1Label.position = 'overImage';
6749        }
6750        if( G.O.thumbnailL1Label.position == 'overImageOnMiddle' ) {
6751          G.O.thumbnailL1Label.valign = 'middle';
6752          G.O.thumbnailL1Label.position = 'overImage';
6753        }
6754        if( G.O.thumbnailL1Label.position == 'overImageOnTop' ) {
6755          G.O.thumbnailL1Label.valign = 'top';
6756          G.O.thumbnailL1Label.position = 'overImage';
6757        }
6758      }
6759
6760      // thumbnails label - level dependant settings
6761      G.O.thumbnailLabel.get = function( opt ) {
6762        if( G.GOM.curNavLevel == 'l1' && G.O.thumbnailL1Label !== undefined && G.O.thumbnailL1Label[opt] !== undefined ) {
6763          return G.O.thumbnailL1Label[opt];
6764        }
6765        else {
6766          return G.O.thumbnailLabel[opt];
6767        }
6768      };
6769      G.O.thumbnailLabel.set = function( opt, value ) {
6770        if( G.GOM.curNavLevel == 'l1' && G.O.thumbnailL1Label !== undefined && G.O.thumbnailL1Label[opt] !== undefined ) {
6771          G.O.thumbnailL1Label[opt]=value;
6772        }
6773        else {
6774          G.O.thumbnailLabel[opt]=value;
6775        }
6776      };
6777
6778      if( G.O.blockList != '' ) { G.blockList = G.O.blockList.toUpperCase().split('|'); }
6779      if( G.O.allowList != '' ) { G.allowList = G.O.allowList.toUpperCase().split('|'); }
6780
6781      if( G.O.albumList2 !== undefined && G.O.albumList2 !== null && G.O.albumList2.constructor === Array  ) {
6782        var l=G.O.albumList2.length;
6783        for(var i=0; i< l; i++ ) {
6784          G.albumList.push(G.O.albumList2[i]);
6785        }
6786        // G.albumList=G.O.albumList.toUpperCase().split('|');
6787      }
6788      if( G.O.albumList2 !== undefined && typeof G.O.albumList2 == 'string'   ) {
6789        G.albumList.push(G.O.albumList2);
6790      }
6791      
6792      
6793      // thumbnail image crop
6794      G.tn.opt.lN.crop = G.O.thumbnailCrop;
6795      G.tn.opt.l1.crop = G.O.thumbnailL1Crop != null ? G.O.thumbnailL1Crop : G.O.thumbnailCrop;
6796
6797
6798      function ThumbnailOpt( lN, l1, opt) {
6799        G.tn.opt.lN[opt] = G.O[lN];
6800        G.tn.opt.l1[opt] = G.O[lN];
6801        if( toType(G.O[l1]) == 'number' ) {
6802          G.tn.opt.l1[opt] = G.O[l1];
6803        }
6804      }
6805      // thumbnail stacks
6806      ThumbnailOpt('thumbnailStacks', 'thumbnailL1Stacks', 'stacks');
6807      // thumbnail stacks translate X
6808      ThumbnailOpt('thumbnailStacksTranslateX', 'thumbnailL1StacksTranslateX', 'stacksTranslateX');
6809      // thumbnail stacks translate Y
6810      ThumbnailOpt('thumbnailStacksTranslateY', 'thumbnailL1StacksTranslateY', 'stacksTranslateY');
6811      // thumbnail stacks translate Z
6812      ThumbnailOpt('thumbnailStacksTranslateZ', 'thumbnailL1StacksTranslateZ', 'stacksTranslateZ');
6813      // thumbnail stacks rotate X
6814      ThumbnailOpt('thumbnailStacksRotateX', 'thumbnailL1StacksRotateX', 'stacksRotateX');
6815      // thumbnail stacks rotate Y
6816      ThumbnailOpt('thumbnailStacksRotateY', 'thumbnailL1StacksRotateY', 'stacksRotateY');
6817      // thumbnail stacks rotate Z
6818      ThumbnailOpt('thumbnailStacksRotateZ', 'thumbnailL1StacksRotateZ', 'stacksRotateZ');
6819      // thumbnail stacks scale
6820      ThumbnailOpt('thumbnailStacksScale', 'thumbnailL1StacksScale', 'stacksScale');
6821      // thumbnail gutter width
6822      // ThumbnailOpt('thumbnailGutterWidth', 'thumbnailL1GutterWidth', 'gutterWidth');
6823      // thumbnail gutter height
6824      // ThumbnailOpt('thumbnailGutterHeight', 'thumbnailL1GutterHeight', 'gutterHeight');
6825      // thumbnail border horizontal
6826      ThumbnailOpt('thumbnailBorderHorizontal', 'thumbnailL1BorderHorizontal', 'borderHorizontal');
6827      // thumbnail border vertical
6828      ThumbnailOpt('thumbnailBorderVertical', 'thumbnailL1BorderVertical', 'borderVertical');
6829      // thumbnail grid base height (for cascading layout)
6830      ThumbnailOpt('thumbnailBaseGridHeight', 'thumbnailL1BaseGridHeight', 'baseGridHeight');
6831      
6832      
6833      // Set same value to all widths
6834      function ResponsiveSetSize( setting, level, v ) {
6835        G.tn.settings[setting][level]['xs'] = v;
6836        G.tn.settings[setting][level]['sm'] = v;
6837        G.tn.settings[setting][level]['me'] = v;
6838        G.tn.settings[setting][level]['la'] = v;
6839        G.tn.settings[setting][level]['xl'] = v;
6840      }
6841
6842      // Get and evaluate responsive values from one option
6843      // Responsive is with syntax: n XSn1 SMn2 MEn3 LAn4 XLn5 (where n is the default value)
6844      // Value 'auto' is accepted for all options, but is handeld only for thumbnail width/height
6845      function ResponsiveOption( option, setting, level ) {
6846        var v = G.O[option];
6847
6848        if( v === undefined || v === null ) { return; }
6849        
6850        // if( toType(v) == 'number' ) {
6851        if( toType(v) == 'number' || v.indexOf(' ') == -1 ) {
6852          // set value for all widths
6853          var vn = 'auto';                              
6854          if( v != 'auto' ) { vn = parseInt(v); }
6855          ResponsiveSetSize( setting, level, vn );
6856        }
6857        else {
6858          var sp = v.split(' ');
6859          if( sp.length > 0 && +sp[0] === +sp[0] ) {          // check if sp[0] is a number
6860            // first value is the default size for all widths
6861            var vn = 'auto';
6862            if( sp[0] != 'auto' ) { vn = parseInt(sp[0]); }
6863            ResponsiveSetSize( setting, level, vn );
6864          }
6865          for( var i = 1; i < sp.length; i++ ) {
6866            if( /^xs|sm|me|la|xl/i.test( sp[i] ) ) {        // regex: i ignores the case and ^ means "starts with"
6867              var wi = sp[i].substring(0, 2).toLowerCase();
6868              var va = sp[i].substring(2);
6869              var vn = 'auto';
6870              if( va != 'auto' ) { vn = parseInt(va); }
6871              G.tn.settings[setting][level][wi] = vn;
6872            }
6873          }
6874        }
6875      }
6876      
6877      ResponsiveOption('thumbnailGutterWidth', 'gutterWidth', 'lN');
6878      ResponsiveOption('thumbnailGutterWidth', 'gutterWidth', 'l1');        // set default values for first level
6879      ResponsiveOption('thumbnailL1GutterWidth', 'gutterWidth', 'l1');
6880      ResponsiveOption('thumbnailGutterHeight', 'gutterHeight', 'lN');
6881      ResponsiveOption('thumbnailGutterHeight', 'gutterHeight', 'l1');      // set default values for first level
6882      ResponsiveOption('thumbnailL1GutterHeight', 'gutterHeight', 'l1');
6883      
6884      // gallery display mode
6885      G.galleryDisplayMode.lN = G.O.galleryDisplayMode.toUpperCase();
6886      G.galleryDisplayMode.l1 = G.O.galleryL1DisplayMode != null ? G.O.galleryL1DisplayMode.toUpperCase() : G.O.galleryDisplayMode.toUpperCase();
6887      
6888      // gallery maximum number of lines of thumbnails
6889      G.galleryMaxRows.lN = G.O.galleryMaxRows;
6890      G.galleryMaxRows.l1 = toType(G.O.galleryL1MaxRows) == 'number' ? G.O.galleryL1MaxRows : G.O.galleryMaxRows;
6891
6892      // gallery last row full
6893      G.galleryLastRowFull.lN = G.O.galleryLastRowFull;
6894      G.galleryLastRowFull.l1 = G.O.galleryL1LastRowFull != null ? G.O.galleryL1LastRowFull : G.O.galleryLastRowFull;
6895      
6896      // gallery sorting
6897      G.gallerySorting.lN = G.O.gallerySorting.toUpperCase();
6898      G.gallerySorting.l1 = G.O.galleryL1Sorting != null ? G.O.galleryL1Sorting.toUpperCase() : G.gallerySorting.lN;
6899      
6900      // gallery display transition
6901      G.galleryDisplayTransition.lN = G.O.galleryDisplayTransition.toUpperCase();
6902      G.galleryDisplayTransition.l1 = G.O.galleryL1DisplayTransition != null ? G.O.galleryL1DisplayTransition.toUpperCase() : G.galleryDisplayTransition.lN;
6903
6904      // gallery display transition duration
6905      G.galleryDisplayTransitionDuration.lN = G.O.galleryDisplayTransitionDuration;
6906      G.galleryDisplayTransitionDuration.l1 = G.O.galleryL1DisplayTransitionDuration != null ? G.O.galleryL1DisplayTransitionDuration : G.galleryDisplayTransitionDuration.lN;
6907      
6908      // gallery max items per album (not for inline/api defined items)
6909      G.galleryMaxItems.lN = G.O.galleryMaxItems;
6910      G.galleryMaxItems.l1 = toType(G.O.galleryL1MaxItems) == 'number' ? G.O.galleryL1MaxItems : G.O.galleryMaxItems;
6911
6912      // gallery filter tags
6913      G.galleryFilterTags.lN = G.O.galleryFilterTags;
6914      G.galleryFilterTags.l1 = G.O.galleryL1FilterTags != null ? G.O.galleryL1FilterTags : G.O.galleryFilterTags;
6915
6916      // gallery filter tags mode
6917      G.galleryFilterTagsMode.lN = G.O.galleryFilterTagsMode;
6918      G.galleryFilterTagsMode.l1 = G.O.galleryL1FilterTagsMode != null ? G.O.galleryL1FilterTagsMode : G.O.galleryFilterTagsMode;
6919      
6920      // gallery pagination
6921      G.O.galleryPaginationMode = G.O.galleryPaginationMode.toUpperCase();
6922
6923      if( toType(G.O.slideshowDelay) == 'number' && G.O.slideshowDelay >= 2000 ) {
6924        G.VOM.slideshowDelay = G.O.slideshowDelay;
6925      }
6926      else {
6927        NanoConsoleLog(G, 'Parameter "slideshowDelay" must be an integer >= 2000 ms.');
6928      }
6929
6930      // gallery display transition
6931      if( typeof G.O.thumbnailDisplayTransition == 'boolean' ) {
6932        if( G.O.thumbnailDisplayTransition === true ) {
6933          G.tn.opt.lN.displayTransition = 'FADEIN';
6934          G.tn.opt.l1.displayTransition = 'FADEIN';
6935        }
6936        else {
6937          G.tn.opt.lN.displayTransition = 'NONE';
6938          G.tn.opt.l1.displayTransition = 'NONE';
6939        }
6940      }
6941
6942      if( G.O.fnThumbnailDisplayEffect !== '' ) {
6943        G.tn.opt.lN.displayTransition = 'CUSTOM';
6944        G.tn.opt.l1.displayTransition = 'CUSTOM';
6945      }
6946      if( G.O.fnThumbnailL1DisplayEffect !== '' ) {
6947        G.tn.opt.l1.displayTransition = 'CUSTOM';
6948      }
6949      
6950
6951      // thumbnail display transition easing
6952			// set default easing
6953      ThumbnailOpt('thumbnailDisplayTransitionEasing', 'thumbnailL1DisplayTransitionEasing', 'displayTransitionEasing');
6954      // parse thumbnail display transition
6955      function thumbnailDisplayTransitionParse( cfg, level ) {
6956        if( typeof cfg == 'string' ) {
6957          var st=cfg.split('_');
6958          if( st.length == 1 ) {
vendor: 130,618 bytes, lines 6959-10296
6959            G.tn.opt[level]['displayTransition'] = cfg.toUpperCase();
6960          }
6961          if( st.length == 2 ) {
6962            G.tn.opt[level]['displayTransition'] = st[0].toUpperCase();
6963            G.tn.opt[level]['displayTransitionStartVal'] = Number(st[1]);
6964          }
6965          if( st.length == 3 ) {
6966            G.tn.opt[level]['displayTransition'] = st[0].toUpperCase();
6967            G.tn.opt[level]['displayTransitionStartVal'] = Number(st[1]);
6968            G.tn.opt[level]['displayTransitionEasing'] = st[2];
6969          }
6970        }
6971      }
6972      thumbnailDisplayTransitionParse( G.O.thumbnailDisplayTransition, 'lN');
6973      thumbnailDisplayTransitionParse( G.O.thumbnailDisplayTransition, 'l1');
6974      thumbnailDisplayTransitionParse( G.O.thumbnailL1DisplayTransition, 'l1');
6975
6976      
6977      // thumbnail display transition duration
6978      ThumbnailOpt('thumbnailDisplayTransitionDuration', 'thumbnailL1DisplayTransitionDuration', 'displayTransitionDuration');
6979      // thumbnail display transition interval duration
6980      ThumbnailOpt('thumbnailDisplayInterval', 'thumbnailL1DisplayInterval', 'displayInterval');
6981      // thumbnail display order
6982      ThumbnailOpt('thumbnailDisplayOrder', 'thumbnailL1DisplayOrder', 'displayOrder');
6983
6984      
6985      // resolution breakpoints --> convert old syntax to new one
6986      if( G.O.thumbnailSizeSM !== undefined ) { G.O.breakpointSizeSM = G.O.thumbnailSizeSM; }
6987      if( G.O.thumbnailSizeME !== undefined ) { G.O.breakpointSizeME = G.O.thumbnailSizeME; }
6988      if( G.O.thumbnailSizeLA !== undefined ) { G.O.breakpointSizeLA = G.O.thumbnailSizeLA; }
6989      if( G.O.thumbnailSizeXL !== undefined ) { G.O.breakpointSizeXL = G.O.thumbnailSizeXL; }
6990
6991      // THUMBNAIL BUILD INIT
6992      //level 1
6993      if( G.O.thumbnailL1BuildInit2 !== undefined ) {
6994        var t1 = G.O.thumbnailL1BuildInit2.split('|');
6995        for( var i = 0; i < t1.length; i++ ) {
6996          var o1 = t1[i].trim().split('_');
6997          if( o1.length == 3 ) {
6998            var i1 = NewTBuildInit();
6999            i1.element = ThumbnailOverEffectsGetCSSElement(o1[0], '');
7000            i1.property = o1[1];
7001            i1.value = o1[2];
7002            G.tn.buildInit.level1.push(i1);
7003          }
7004        }
7005      }
7006      //level N
7007      if( G.O.thumbnailBuildInit2 !== undefined ) {
7008        var t1 = G.O.thumbnailBuildInit2.split('|');
7009        for( var i = 0; i < t1.length; i++ ) {
7010          var o1 = t1[i].trim().split('_');
7011          if( o1.length == 3 ) {
7012            var i1 = NewTBuildInit();
7013            i1.element = ThumbnailOverEffectsGetCSSElement(o1[0], '');
7014            i1.property = o1[1];
7015            i1.value = o1[2];
7016            G.tn.buildInit.std.push(i1);
7017          }
7018        }
7019      }
7020
7021      
7022      // THUMBNAIL HOVER EFFETCS
7023      
7024      // thumbnails hover effects - Level1
7025      var tL1HE = G.O.thumbnailL1HoverEffect2;
7026      if( tL1HE !== undefined ) {
7027        switch( toType(tL1HE) ) {
7028          case 'string': {
7029              let tmp = tL1HE.split('|');
7030              for(var i = 0; i < tmp.length; i++) {
7031                let oDef = NewTHoverEffect();
7032                oDef = ThumbnailHoverEffectExtract( tmp[i].trim(), oDef );
7033                if(  oDef != null ) {
7034                  G.tn.hoverEffects.level1.push(oDef);
7035                }
7036              }
7037              break;
7038            }
7039          case 'object': {
7040              let oDef = NewTHoverEffect();
7041              oDef = jQuery.extend(oDef,tL1HE);
7042              oDef = ThumbnailHoverEffectExtract( oDef.name, oDef );
7043              if(  oDef != null ) {
7044                G.tn.hoverEffects.level1.push(oDef);
7045              }
7046              break;
7047            }
7048          case 'array': {
7049              for(var i = 0; i < tL1HE.length; i++) {
7050                let oDef = NewTHoverEffect();
7051                oDef = jQuery.extend(oDef,tL1HE[i]);
7052                oDef = ThumbnailHoverEffectExtract( oDef.name, oDef );
7053                if(  oDef != null ) {
7054                  G.tn.hoverEffects.level1.push(oDef);
7055                }
7056              }
7057              break;
7058            }
7059          case 'null':
7060            break;
7061          default:
7062            NanoAlert(G, 'incorrect parameter for "thumbnailL1HoverEffect2".');
7063        }
7064      }
7065      G.tn.hoverEffects.level1 = ThumbnailOverEffectsPreset(G.tn.hoverEffects.level1);
7066  
7067      // thumbnails hover effects - other levels
7068      var tHE = G.O.thumbnailHoverEffect2;
7069      switch( toType(tHE) ) {
7070        case 'string': {
7071            let tmp = tHE.split('|');
7072            for(var i = 0; i < tmp.length; i++) {
7073              let oDef = NewTHoverEffect();
7074              oDef = ThumbnailHoverEffectExtract( tmp[i].trim(), oDef );
7075              if(  oDef != null ) {
7076                G.tn.hoverEffects.std.push(oDef);
7077              }
7078            }
7079            break;
7080          }
7081        case 'object': {
7082            let oDef = NewTHoverEffect();
7083            oDef = jQuery.extend(oDef, tHE);
7084            oDef = ThumbnailHoverEffectExtract( oDef.name, oDef );
7085            if(  oDef != null ) {
7086              G.tn.hoverEffects.std.push(oDef);
7087            }
7088            break;
7089          }
7090        case 'array': {
7091            for(var i = 0; i < tHE.length; i++) {
7092              let oDef = NewTHoverEffect();
7093              oDef = jQuery.extend(oDef,tHE[i]);
7094              oDef = ThumbnailHoverEffectExtract( oDef.name, oDef );
7095              if(  oDef!= null ) {
7096                G.tn.hoverEffects.std.push(oDef);
7097              }
7098            }
7099            break;
7100          }
7101        case 'null':
7102          break;
7103        default:
7104          NanoAlert(G, 'incorrect parameter for "thumbnailHoverEffect2".');
7105      }
7106      G.tn.hoverEffects.std = ThumbnailOverEffectsPreset(G.tn.hoverEffects.std);
7107
7108      
7109      if( G.O.touchAnimationL1 == undefined ) {
7110        G.O.touchAnimationL1 = G.O.touchAnimation;
7111      }
7112      
7113      // disable thumbnail touch animation when no hover effect defined
7114      if( G.tn.hoverEffects.std.length == 0 ) {
7115        if( G.tn.hoverEffects.level1.length == 0 ) {
7116          G.O.touchAnimationL1 = false;
7117        }
7118        G.O.touchAnimation = false;
7119      }      
7120      
7121      
7122      // thumbnail sizes
7123      if( G.O.thumbnailHeight == 0 || G.O.thumbnailHeight == ''  ) { G.O.thumbnailHeight = 'auto'; }
7124      if( G.O.thumbnailWidth == 0 || G.O.thumbnailWidth == '' ) { G.O.thumbnailWidth = 'auto'; }
7125      if( G.O.thumbnailL1Height == 0 || G.O.thumbnailL1Height == '' ) { G.O.thumbnailL1Height = 'auto'; }
7126      if( G.O.thumbnailL1Width == 0 || G.O.thumbnailL1Width == '' ) { G.O.thumbnailL1Width = 'auto'; }
7127
7128      // RETRIEVE ALL THUMBNAIL SIZES
7129      // ThumbnailSizes( 'thumbnailWidth', false, 'width');
7130      // ThumbnailSizes( 'thumbnailL1Width', true, 'width');
7131      // ThumbnailSizes( 'thumbnailHeight', false, 'height');
7132      // ThumbnailSizes( 'thumbnailL1Height', true, 'height');
7133      ResponsiveOption('thumbnailWidth', 'width', 'lN');
7134      ResponsiveOption('thumbnailWidth', 'width', 'l1');
7135      ResponsiveOption('thumbnailL1Width', 'width', 'l1');
7136      ResponsiveOption('thumbnailHeight', 'height', 'lN');
7137      ResponsiveOption('thumbnailHeight', 'height', 'l1');
7138      ResponsiveOption('thumbnailL1Height', 'height', 'l1');
7139
7140      
7141      G.O.thumbnailLabelHeight = parseInt(G.O.thumbnailLabelHeight);
7142
7143      
7144      // retrieve all mosaic layout patterns
7145      // default pattern
7146      if( G.O.galleryMosaic != undefined ) {
7147        // clone object
7148        G.tn.settings.mosaic.l1.xs = JSON.parse(JSON.stringify(G.O.galleryMosaic));
7149        G.tn.settings.mosaic.l1.sm = JSON.parse(JSON.stringify(G.O.galleryMosaic));
7150        G.tn.settings.mosaic.l1.me = JSON.parse(JSON.stringify(G.O.galleryMosaic));
7151        G.tn.settings.mosaic.l1.la = JSON.parse(JSON.stringify(G.O.galleryMosaic));
7152        G.tn.settings.mosaic.l1.xl = JSON.parse(JSON.stringify(G.O.galleryMosaic));
7153        G.tn.settings.mosaic.lN.xs = JSON.parse(JSON.stringify(G.O.galleryMosaic));
7154        G.tn.settings.mosaic.lN.sm = JSON.parse(JSON.stringify(G.O.galleryMosaic));
7155        G.tn.settings.mosaic.lN.me = JSON.parse(JSON.stringify(G.O.galleryMosaic));
7156        G.tn.settings.mosaic.lN.la = JSON.parse(JSON.stringify(G.O.galleryMosaic));
7157        G.tn.settings.mosaic.lN.xl = JSON.parse(JSON.stringify(G.O.galleryMosaic));
7158        G.tn.settings.mosaicCalcFactor('l1', 'xs');
7159        G.tn.settings.mosaicCalcFactor('l1', 'sm');
7160        G.tn.settings.mosaicCalcFactor('l1', 'me');
7161        G.tn.settings.mosaicCalcFactor('l1', 'la');
7162        G.tn.settings.mosaicCalcFactor('l1', 'xl');
7163        G.tn.settings.mosaicCalcFactor('lN', 'xs');
7164        G.tn.settings.mosaicCalcFactor('lN', 'sm');
7165        G.tn.settings.mosaicCalcFactor('lN', 'me');
7166        G.tn.settings.mosaicCalcFactor('lN', 'la');
7167        G.tn.settings.mosaicCalcFactor('lN', 'xl');
7168      }
7169      if( G.O.galleryL1Mosaic != undefined ) {
7170        // default L1 pattern
7171        G.tn.settings.mosaic.l1.xs = JSON.parse(JSON.stringify(G.O.galleryL1Mosaic));
7172        G.tn.settings.mosaic.l1.sm = JSON.parse(JSON.stringify(G.O.galleryL1Mosaic));
7173        G.tn.settings.mosaic.l1.me = JSON.parse(JSON.stringify(G.O.galleryL1Mosaic));
7174        G.tn.settings.mosaic.l1.la = JSON.parse(JSON.stringify(G.O.galleryL1Mosaic));
7175        G.tn.settings.mosaic.l1.xl = JSON.parse(JSON.stringify(G.O.galleryL1Mosaic));
7176        G.tn.settings.mosaicCalcFactor('l1', 'xs');
7177        G.tn.settings.mosaicCalcFactor('l1', 'sm');
7178        G.tn.settings.mosaicCalcFactor('l1', 'me');
7179        G.tn.settings.mosaicCalcFactor('l1', 'la');
7180        G.tn.settings.mosaicCalcFactor('l1', 'xl');
7181      }
7182
7183			var lst=['xs','sm','me','la','xl'];
7184			// retrieve responsive mosaic definition for levels l1 & lN
7185      for( var w = 0; w < lst.length; w++ ) {
7186				if( G.O['galleryMosaic' + lst[w].toUpperCase()] != undefined ) {
7187          G.tn.settings.mosaic.lN[lst[w]] = JSON.parse(JSON.stringify( G.O['galleryMosaic' + lst[w].toUpperCase()] ));
7188          G.tn.settings.mosaic.l1[lst[w]] = JSON.parse(JSON.stringify( G.O['galleryMosaic' + lst[w].toUpperCase()] ));
7189          G.tn.settings.mosaicCalcFactor('lN',lst[w]);
7190          G.tn.settings.mosaicCalcFactor('l1', lst[w]);
7191        }
7192      }
7193			// retrieve responsive mosaic definition for level l1
7194      for( var w = 0; w < lst.length; w++ ) {
7195				if( G.O['galleryL1Mosaic' + lst[w].toUpperCase()] != undefined ) {
7196          G.tn.settings.mosaic.l1[lst[w]] = JSON.parse(JSON.stringify( G.O['galleryL1Mosaic' + lst[w].toUpperCase()] ));
7197          G.tn.settings.mosaicCalcFactor('l1', lst[w]);
7198        }
7199      }
7200      
7201      G.O.imageTransition = G.O.imageTransition.toUpperCase();
7202
7203      G.layout.SetEngine();
7204      
7205      // init plugins
7206      switch( G.O.kind ) {
7207        // MARKUP / API
7208        case '':
7209          break;
7210        // JSON, Flickr, Picasa, ...
7211        default:
7212        jQuery.nanogallery2['data_' + G.O.kind](G, 'Init' );
7213      }
7214
7215    }
7216
7217    // HOVER EFFECTS
7218    function ThumbnailHoverEffectExtract( name, effect) {
7219      var easings = [ 'easeInQuad', 'easeOutQuad', 'easeInOutQuad', 'easeInCubic', 'easeOutCubic', 'easeInOutCubic', 'easeInQuart', 'easeOutQuart', 'easeInOutQuart', 'easeInQuint', 'easeOutQuint', 'easeInOutQuint', 'easeInSine', 'easeOutSine', 'easeInOutSine', 'easeInExpo', 'easeOutExpo', 'easeInOutExpo', 'easeInCirc', 'easeOutCirc', 'easeInOutCirc', 'easeOutBounce', 'easeInBack', 'easeOutBack', 'easeInOutBack', 'elastic', 'bounce'];
7220    
7221      var sp = name.split('_');
7222      if( sp.length >= 4 ) {
7223        // var oDef=NewTHoverEffect();
7224        effect.name = '';
7225        effect.type = sp[1];
7226        effect.from = sp[2];
7227        effect.to = sp[3];
7228        if( sp.length >= 5 ) {
7229          // effect.duration=sp[4];
7230
7231          for( var n = 4; n < sp.length; n++ ) {
7232            var v = sp[n];
7233            
7234            // check if an easing name
7235            var foundEasing = false;
7236            for( var e = 0; e < easings.length; e++) {
7237              if( v == easings[e] ) {
7238                foundEasing = true;
7239                effect.easing = v;
7240                break;
7241              }
7242            }
7243            if( foundEasing === true ) {
7244              continue;
7245            }
7246            
7247            v = v.toUpperCase();
7248            
7249            if( v == 'HOVERIN' ) {
7250              effect.hoverout = false;
7251              continue;
7252            }
7253            if( v == 'HOVEROUT' ) {
7254              effect.hoverin = false;
7255              continue;
7256            }
7257            
7258            if( v == 'KEYFRAME' ) {
7259              effect.firstKeyframe = false;
7260              continue;
7261            }
7262            
7263            var num = parseInt(v.replace(/[^0-9\.]/g, ''), 10);   // extract a number if one exists
7264
7265            if( num > 0 ) {
7266              // the string contains a numbers > 0
7267              if( v.indexOf('DURATION') >= 0 ) {
7268                effect.duration = num;
7269                continue;
7270              }
7271              if( v.indexOf('DURATIONBACK') >= 0 ) {
7272                effect.durationBack = num;
7273                continue;
7274              }
7275              if( v.indexOf('DELAY') >= 0 ) {
7276                effect.delay = num;
7277                continue;
7278              }
7279              if( v.indexOf('DELAYBACK') >= 0 ) {
7280                effect.delayBack = num;
7281                continue;
7282              }
7283              
7284              // no parameter name found -> default is duration
7285              effect.duration = num;
7286            }
7287          }
7288        }        
7289        effect.element = ThumbnailOverEffectsGetCSSElement(sp[0], effect.type);
7290        
7291      }
7292      else {
7293        effect.name = name;
7294        // NanoAlert(G, 'incorrect parameter for "thumbnailHoverEffect": ' + name);
7295        // return null;
7296      }
7297      return effect;
7298    }
7299    
7300    
7301    function ThumbnailOverEffectsGetCSSElement( element, property ) {
7302        
7303         var elts = {
7304          'image':        '.nGY2GThumbnailImage',
7305          'thumbnail':    '.nGY2GThumbnail',
7306          'label':        '.nGY2GThumbnailLabel',
7307          'title':        '.nGY2GThumbnailTitle',
7308          'description':  '.nGY2GThumbnailDescription',
7309          'tools':        '.nGY2GThumbnailIcons',
7310          'customlayer':  '.nGY2GThumbnailCustomLayer',
7311          'default':      'nGY2GThumbnailImage'
7312        };
7313        return (elts[element] || elts['default']);
7314        
7315
7316    }
7317    
7318    // convert preset hover effects (nanoGALLERY) to new ones (nanogallery2)
7319    function ThumbnailOverEffectsPreset( effects ) {
7320
7321      // COMPATIBILITY WITH nanoGALLERY
7322      // OK:
7323      //  'borderLighter', 'borderDarker', 'scale120', 'labelAppear', 'labelAppear75', 'labelOpacity50', 'scaleLabelOverImage'
7324      //  'overScale', 'overScaleOutside', 'descriptionAppear'
7325      //  'slideUp', 'slideDown', 'slideRight', 'slideLeft'
7326      //  'imageScale150', 'imageScaleIn80', 'imageScale150Outside', 'imageSlideUp', 'imageSlideDown', 'imageSlideRight', 'imageSlideLeft'
7327      //  'labelSlideUpTop', 'labelSlideUp', 'labelSlideDown', 'descriptionSlideUp'
7328      // KO:
7329      //  'labelSplit4', 'labelSplitVert', 'labelAppearSplit4', 'labelAppearSplitVert' 
7330      // TODO:
7331      //  'rotateCornerBL', 'rotateCornerBR', 'imageSplit4', 'imageSplitVert', 'imageRotateCornerBL', 'imageRotateCornerBR', 'imageFlipHorizontal', 'imageFlipVertical'
7332
7333    
7334   
7335      var newEffects=[];
7336      for( var i=0; i< effects.length; i++ ) {
7337        switch( effects[i].name.toUpperCase() ) {
7338          case 'BORDERLIGHTER': {
7339              let rgb = ColorHelperToRGB(GalleryThemeGetCurrent().thumbnail.borderColor);
7340              let name = 'thumbnail_borderColor_'+rgb+'_'+ShadeBlendConvert(0.5, rgb );
7341              newEffects.push(ThumbnailHoverEffectExtract(name, effects[i]));
7342              break;
7343            }
7344          case 'BORDERDARKER': {
7345              let rgb = ColorHelperToRGB(GalleryThemeGetCurrent().thumbnail.borderColor);
7346              let name = 'thumbnail_borderColor_'+rgb+'_'+ShadeBlendConvert(-0.5, rgb );
7347              newEffects.push(ThumbnailHoverEffectExtract(name, effects[i]));
7348              break;
7349            }
7350          case 'SCALE120':
7351            newEffects.push(ThumbnailHoverEffectExtract('thumbnail_scale_1.00_1.20', effects[i]));
7352            break;
7353          case 'LABELAPPEAR':
7354          case 'LABELAPPEAR75':
7355            newEffects.push(ThumbnailHoverEffectExtract('label_opacity_0.00_1.00', effects[i]));
7356            break;
7357          case 'TOOLSAPPEAR':
7358            newEffects.push(ThumbnailHoverEffectExtract('tools_opacity_0_1', effects[i]));
7359            break;
7360          case 'TOOLSSLIDEDOWN':
7361            newEffects.push(ThumbnailHoverEffectExtract('tools_translateY_-100%_0%', effects[i]));
7362            break;
7363          case 'TOOLSSLIDEUP':
7364            newEffects.push(ThumbnailHoverEffectExtract('tools_translateY_100%_0%', effects[i]));
7365            break;
7366          case 'LABELOPACITY50':
7367            newEffects.push(ThumbnailHoverEffectExtract('label_opacity_1.00_0.50', effects[i]));
7368            break;
7369          case 'LABELSLIDEUPTOP':
7370          case 'LABELSLIDEUP':
7371            newEffects.push(ThumbnailHoverEffectExtract('label_translateY_100%_0%', effects[i]));
7372            newEffects.push(ThumbnailHoverEffectExtract('label_translateY_100%_0%', effects[i]));
7373            break;
7374          case 'LABELSLIDEDOWN':
7375            newEffects.push(ThumbnailHoverEffectExtract('label_translateY_-100%_0%', effects[i]));
7376            break;
7377          case 'SCALELABELOVERIMAGE':
7378            newEffects.push(ThumbnailHoverEffectExtract('label_scale_0.00_1.00', effects[i]));
7379            var n = cloneJSObject(effects[i]);
7380            newEffects.push(ThumbnailHoverEffectExtract('image_scale_1.00_0.00', n));
7381            break;
7382          case 'OVERSCALE':
7383          case 'OVERSCALEOUTSIDE':
7384            //var name = 'label_scale_0_100';
7385            newEffects.push(ThumbnailHoverEffectExtract('label_scale_2.00_1.00', effects[i]));
7386            var n = cloneJSObject(effects[i]);
7387            newEffects.push(ThumbnailHoverEffectExtract('label_opacity_0.00_1.00', n));
7388            n = cloneJSObject(effects[i]);
7389            newEffects.push(ThumbnailHoverEffectExtract('image_scale_1.00_0.00', n));
7390            n = cloneJSObject(effects[i]);
7391            newEffects.push(ThumbnailHoverEffectExtract('image_opacity_1.00_0.00', n));
7392            break;
7393          case 'DESCRIPTIONAPPEAR':
7394            newEffects.push(ThumbnailHoverEffectExtract('description_opacity_0_1', effects[i]));
7395            break;
7396          case 'SLIDERIGHT':
7397            newEffects.push(ThumbnailHoverEffectExtract('image_translateX_0%_100%', effects[i]));
7398            newEffects.push(ThumbnailHoverEffectExtract('label_translateX_-100%_0%', cloneJSObject(effects[i])));
7399            break;
7400          case 'SLIDELEFT':
7401            newEffects.push(ThumbnailHoverEffectExtract('image_translateX_0%_-100%', effects[i]));
7402            newEffects.push(ThumbnailHoverEffectExtract('label_translateX_100%_0%', cloneJSObject(effects[i])));
7403            break;
7404          case 'SLIDEUP':
7405            newEffects.push(ThumbnailHoverEffectExtract('image_translateY_0%_-100%', effects[i]));
7406            newEffects.push(ThumbnailHoverEffectExtract('label_translateY_100%_0%', cloneJSObject(effects[i])));
7407            break;
7408          case 'SLIDEDOWN':
7409            newEffects.push(ThumbnailHoverEffectExtract('image_translateY_0%_100%', effects[i]));
7410            newEffects.push(ThumbnailHoverEffectExtract('label_translateY_-100%_0%', cloneJSObject(effects[i])));
7411            break;
7412          case 'IMAGESCALE150':
7413          case 'IMAGESCALE150OUTSIDE':
7414            newEffects.push(ThumbnailHoverEffectExtract('image_scale_1.00_1.50', effects[i]));
7415            break;
7416          case 'IMAGESCALEIN80':
7417            newEffects.push(ThumbnailHoverEffectExtract('image_scale_1.20_1.00', effects[i]));
7418            break;
7419          case 'IMAGESLIDERIGHT':
7420            newEffects.push(ThumbnailHoverEffectExtract('image_translateX_0%_100%', effects[i]));
7421            break;
7422          case 'IMAGESLIDELEFT':
7423            newEffects.push(ThumbnailHoverEffectExtract('image_translateX_0%_-100%', effects[i]));
7424            break;
7425          case 'IMAGESLIDEUP':
7426            newEffects.push(ThumbnailHoverEffectExtract('image_translateY_0%_-100%', effects[i]));
7427            break;
7428          case 'IMAGESLIDEDOWN':
7429            newEffects.push(ThumbnailHoverEffectExtract('image_translateY_0%_100%', effects[i]));
7430            break;
7431          case 'LABELSLIDEUPDOWN':
7432            newEffects.push(ThumbnailHoverEffectExtract('label_translateY_0%_100%', effects[i]));
7433            break;
7434          case 'DESCRIPTIONSLIDEUP':
7435            newEffects.push(ThumbnailHoverEffectExtract('description_translateY_110%_0%', effects[i]));
7436            break;
7437
7438          case 'IMAGEBLURON':
7439            newEffects.push(ThumbnailHoverEffectExtract('image_blur_2.00px_0.00px', effects[i]));
7440            break;
7441          case 'IMAGEBLUROFF':
7442            newEffects.push(ThumbnailHoverEffectExtract('image_blur_0.00px_2.00px', effects[i]));
7443            break;
7444          case 'IMAGEGRAYON':
7445            newEffects.push(ThumbnailHoverEffectExtract('image_grayscale_0%_100%', effects[i]));
7446            break;
7447          case 'IMAGEGRAYOFF':
7448            newEffects.push(ThumbnailHoverEffectExtract('image_grayscale_100%_0%', effects[i]));
7449            break;
7450          case 'IMAGESEPIAON':
7451            newEffects.push(ThumbnailHoverEffectExtract('image_sepia_100%_1%', effects[i]));
7452            break;
7453          case 'IMAGESEPIAOFF':
7454            newEffects.push(ThumbnailHoverEffectExtract('image_sepia_1%_100%', effects[i]));
7455            break;
7456            
7457          default:
7458            newEffects.push(effects[i]);
7459            break;
7460        }
7461      }
7462      
7463      return newEffects;
7464    }
7465
7466
7467    // Thumbnail hover effect definition
7468    function NewTHoverEffect() {
7469      var oDef={ 
7470          name:           '',
7471          element:        '',               // element class
7472          type:           '',               
7473          from:           '',               // start value
7474          to:             '',               // end value
7475          hoverin:        true,
7476          hoverout:       true,
7477          firstKeyframe:  true,
7478          delay:          0,
7479          delayBack:      0,
7480          duration:       400,
7481          durationBack:   300,
7482          easing:         'easeOutQuart',
7483          easingBack:     'easeOutQuart',
7484          animParam:      null
7485        };
7486      return oDef;
7487    }
7488
7489    function NewTBuildInit() {
7490      // to set CSS properties
7491      var oDef={ element: '', property: '', value: '' };
7492      return oDef;
7493    }
7494
7495    
7496    function ThumbnailStyle( cfg, level) {
7497
7498      switch( cfg.position ){
7499        case 'onBottom' :
7500          G.tn.style[level]['label'] = 'bottom:0; ';
7501          break;
7502        case 'right' :
7503          switch( cfg.valign ) {
7504              case 'top':
7505                G.tn.style[level]['label'] = 'top:0; position:absolute; left: 50%;';
7506                break;
7507              case 'middle':
7508                G.tn.style[level]['label'] = 'top:0; bottom:0; left: 50%;';
7509                G.tn.style[level]['title'] = 'position:absolute; bottom:50%;';
7510                G.tn.style[level]['desc'] = 'position:absolute; top:50%;';
7511                break;
7512              case 'bottom':
7513              default:
7514                G.tn.style[level].label = 'bottom:0; position:absolute; left: 50%;';
7515                G.tn.style[level].title = 'position:absolute;bottom:0;';
7516                break;
7517          }
7518          break;
7519        case 'custom':
7520          break;
7521        default:
7522        case 'overImage' :
7523          switch( cfg.valign ) {
7524              case 'top':
7525                G.tn.style[level]['label'] = 'top:0; position:absolute;';
7526                break;
7527              case 'middle':
7528                G.tn.style[level]['label'] = 'top:0; bottom:0;';
7529                G.tn.style[level]['title'] = 'position:absolute; bottom:50%;';
7530                G.tn.style[level]['desc'] = 'position:absolute; top:50%;';
7531                break;
7532              case 'bottom':
7533              default:
7534                // G.O.thumbnailLabel.position = 'overImageOnBottom';
7535                G.tn.style[level].label = 'bottom:0; position:absolute;';
7536                break;
7537          }
7538        
7539        // case 'overImageOnTop' :
7540          // G.tn.style[level]['label'] = 'top:0; position:absolute;';
7541          // break;
7542        // case 'overImageOnMiddle' :
7543          // G.tn.style[level]['label'] = 'top:0; bottom:0;';
7544          // G.tn.style[level]['title'] = 'position:absolute; bottom:50%;';
7545          // G.tn.style[level]['desc'] = 'position:absolute; top:50%;';
7546          // break;
7547        // case 'right' :
7548        // case 'custom' :
7549          // break;
7550        // case 'overImageOnBottom' :
7551        // default :
7552          // G.O.thumbnailLabel.position = 'overImageOnBottom';
7553          // G.tn.style[level].label = 'bottom:0; position:absolute;';
7554          // break;
7555      }
7556      
7557      // if( G.layout.engine != 'CASCADING' ) {
7558      if( cfg.position != 'onBottom' ) {
7559        // multi-line
7560        if( cfg.titleMultiLine ) {
7561          G.tn.style[level]['title'] += 'white-space:normal;';
7562        }
7563        if( cfg.descriptionMultiLine ) {
7564          G.tn.style[level]['desc'] += 'white-space:normal;';
7565        }
7566      }
7567      
7568      // horizontal alignement
7569      switch( cfg.align ) {
7570        case 'right':
7571            G.tn.style[level].label += 'text-align:right;';
7572          break;
7573        case 'left':
7574            G.tn.style[level].label += 'text-align:left;';
7575          break;
7576        default:
7577            G.tn.style[level].label += 'text-align:center;';
7578          break;
7579      }
7580      
7581      
7582      if( cfg.titleFontSize != undefined && cfg.titleFontSize != '' ) {
7583        G.tn.style[level].title += 'font-size:' + cfg.titleFontSize + ';';
7584      }
7585      if( cfg.descriptionFontSize != undefined && cfg.descriptionFontSize != '' ) {
7586        G.tn.style[level].desc += 'font-size:' + cfg.descriptionFontSize + ';';
7587      }
7588      
7589      if( cfg.displayDescription == false ) {
7590        G.tn.style[level].desc += 'display:none;';
7591      }
7592    }
7593
7594    
7595    // cache some thumbnail settings
7596    function ThumbnailDefCaches() {
7597      // thumbnail content CSS styles
7598
7599      // settings for level L1 and LN
7600      ThumbnailStyle( G.O.thumbnailLabel, 'lN');
7601      if( G.O.thumbnailL1Label !== undefined ) {
7602        ThumbnailStyle( G.O.thumbnailL1Label, 'l1');
7603      }
7604      else {
7605        ThumbnailStyle( G.O.thumbnailLabel, 'l1');
7606      }
7607
7608      if( G.O.thumbnailL1Label && G.O.thumbnailL1Label.display ) {
7609        // settings for level L1
7610        ThumbnailStyle( G.O.thumbnailL1Label, 'l1');
7611      }
7612      
7613
7614      // default thumbnail sizes levels l1 and lN
7615      var lst=['xs','sm','me','la','xl'];
7616      for( var i = 0; i < lst.length; i++ ) {
7617        var w = G.tn.settings.width.lN[lst[i]];
7618        if( w != 'auto' ) {
7619          G.tn.defaultSize.width.lN[lst[i]] = w;
7620          G.tn.defaultSize.width.l1[lst[i]] = w;
7621        }
7622        else {
7623          var h = G.tn.settings.height.lN[lst[i]];
7624          G.tn.defaultSize.width.lN[lst[i]] = h;      // dynamic width --> set height value as default for the width
7625          G.tn.defaultSize.width.l1[lst[i]] = h;      // dynamic width --> set height value as default
7626        }
7627      }
7628      for( var i = 0; i < lst.length; i++ ) {
7629        var h = G.tn.settings.height.lN[lst[i]];
7630        if( h != 'auto' ) {
7631          // grid or justified layout
7632          G.tn.defaultSize.height.lN[lst[i]] = h;  //+G.tn.labelHeight.get();
7633          G.tn.defaultSize.height.l1[lst[i]] = h;  //+G.tn.labelHeight.get();
7634        }
7635        else {
7636          var w = G.tn.settings.width.lN[lst[i]];
7637          G.tn.defaultSize.height.lN[lst[i]] = w;      // dynamic height --> set width value as default for the height
7638          G.tn.defaultSize.height.l1[lst[i]] = w;      // dynamic height --> set width value as default
7639        }
7640      }
7641
7642      // default thumbnail sizes levels l1
7643      for( var i = 0; i < lst.length; i++ ) {
7644        var w = G.tn.settings.width.l1[lst[i]];
7645        if( w != 'auto' ) {
7646          G.tn.defaultSize.width.l1[lst[i]] = w;
7647        }
7648        else {
7649          var h = G.tn.settings.height.l1[lst[i]];
7650          G.tn.defaultSize.width.l1[lst[i]] = h;      // dynamic width --> set height value as default
7651        }
7652      }
7653      for( var i = 0; i < lst.length; i++ ) {
7654        var h = G.tn.settings.height.l1[lst[i]];
7655        if( h != 'auto' ) {
7656          // grid or justified layout
7657          G.tn.defaultSize.height.l1[lst[i]] = h;  //+G.tn.labelHeight.get();
7658        }
7659        else {
7660          var w = G.tn.settings.width.l1[lst[i]];
7661          G.tn.defaultSize.height.l1[lst[i]]= w ;      // dynamic height --> set width value as default
7662        }
7663      }
7664      
7665    }
7666    
7667
7668    //
7669    function GalleryThemeGetCurrent() {
7670
7671      var cs=null;
7672      switch(toType(G.O.galleryTheme)) {
7673        case 'object':    // user custom color scheme object 
7674          cs = G.galleryTheme_dark;  // default color scheme
7675          jQuery.extend(true,cs,G.O.galleryTheme);
7676          break;
7677        case 'string':    // name of an internal defined color scheme
7678          switch( G.O.galleryTheme ) {
7679            case 'light':
7680              cs = G.galleryTheme_light;
7681              break;
7682            case 'default':
7683            case 'dark':
7684            case 'none':
7685            default:
7686              cs = G.galleryTheme_dark;
7687          }
7688          break;
7689        default:
7690          cs = G.galleryTheme_dark;
7691      }
7692      return cs;
7693    }
7694    
7695    // ##### BREADCRUMB/THUMBNAIL COLOR SCHEME #####
7696    function SetGalleryTheme() {
7697    
7698      if( typeof G.O.colorScheme  !== 'undefined' ) {
7699        G.O.galleryTheme = G.O.colorScheme;
7700      }
7701
7702      var cs = null;
7703      var galleryTheme = '';
7704      switch(toType(G.O.galleryTheme)) {
7705        case 'object':    // user custom color scheme object 
7706          cs = G.galleryTheme_dark;  // default color scheme
7707          jQuery.extend(true,cs,G.O.galleryTheme);
7708          galleryTheme='nanogallery_gallerytheme_custom_' + G.baseEltID;
7709          break;
7710        case 'string':    // name of an internal defined color scheme
7711          switch( G.O.galleryTheme ) {
7712            case 'light':
7713              cs = G.galleryTheme_light;
7714              galleryTheme='nanogallery_gallerytheme_light_' + G.baseEltID;
7715              break;
7716            case 'default':
7717            case 'dark':
7718            case 'none':
7719            default:
7720              cs = G.galleryTheme_dark;
7721              galleryTheme='nanogallery_gallerytheme_dark_' + G.baseEltID;
7722          }
7723          break;
7724        default:
7725          NanoAlert(G, 'Error in galleryTheme parameter.');
7726          return;
7727      }
7728
7729      //var s1='.nanogallery_theme_'+G.O.theme+' ';
7730      var s1='.' + galleryTheme + ' ';
7731    
7732      // navigation bar
7733      var c = cs.navigationBar;
7734      var s=s1+'.nGY2Navigationbar { background:'+c.background+'; }'+'\n';
7735      if( c.border !== undefined && c.border !== '' ) { s+=s1+'.nGY2Navigationbar { border:'+c.border+'; }'+'\n'; }
7736      if( c.borderTop !== undefined && c.borderTop !== '' ) { s+=s1+'.nGY2Navigationbar { border-top:'+c.borderTop+'; }'+'\n'; }
7737      if( c.borderBottom !== undefined && c.borderBottom !== '' ) { s+=s1+'.nGY2Navigationbar { border-bottom:'+c.borderBottom+'; }'+'\n'; }
7738      if( c.borderRight !== undefined && c.borderRight !== '' ) { s+=s1+'.nGY2Navigationbar { border-right:'+c.borderRight+'; }'+'\n'; }
7739      if( c.borderLeft !== undefined && c.borderLeft !== '' ) { s+=s1+'.nGY2Navigationbar { border-left:'+c.borderLeft+'; }'+'\n'; }
7740      
7741      // navigation bar - breadcrumb
7742      var c = cs.navigationBreadcrumb;
7743      s+=s1+'.nGY2Breadcrumb { background:'+c.background+'; border-radius:'+c.borderRadius+'; }'+'\n';
7744      s+=s1+'.nGY2Breadcrumb .oneItem  { color:'+c.color+'; }'+'\n';
7745      s+=s1+'.nGY2Breadcrumb .oneItem:hover { color:'+c.colorHover+'; }'+'\n';
7746
7747      // navigation bar - tag filter
7748      var c = cs.navigationFilter;
7749      s+=s1+'.nGY2NavFilterUnselected { color:'+c.color+'; background:'+c.background+'; border-radius:'+c.borderRadius+'; }'+'\n';
7750      s+=s1+'.nGY2NavFilterSelected { color:'+c.colorSelected+'; background:'+c.backgroundSelected+'; border-radius:'+c.borderRadius+'; }'+'\n';
7751      s+=s1+'.nGY2NavFilterSelectAll { color:'+c.colorSelected+'; background:'+c.background+'; border-radius:'+c.borderRadius+'; }'+'\n';
7752
7753      // navigation bar - pagination next/previous
7754      var c = cs.navigationPagination;
7755      s+=s1+'.nGY2NavPagination { color:'+c.color+'; background:'+c.background+'; border-radius:'+c.borderRadius+'; }'+'\n';
7756      s+=s1+'.nGY2NavPagination:hover { color:'+c.colorHover+'; }'+'\n';
7757      
7758      // thumbnails
7759      var c = cs.thumbnail;
7760      // s+=s1+'.nGY2GThumbnail { border-radius: '+c.borderRadius+'; background:'+c.background+'; border-color:'+c.borderColor+'; border-top-width:'+G.tn.opt.Get('borderVertical')+'px; border-right-width:'+G.tn.opt.Get('borderHorizontal')+'px; border-bottom-width:'+G.tn.opt.Get('borderVertical')+'px; border-left-width:'+G.tn.opt.Get('borderHorizontal')+'px;}'+'\n';
7761      s+=s1+'.nGY2GThumbnail { border-radius: '+c.borderRadius+'; background:'+c.background+'; border-color:'+c.borderColor+'; }'+'\n';
7762      s+=s1+'.nGY2GThumbnail_l1 { border-top-width:'+G.tn.opt.l1.borderVertical+'px; border-right-width:'+G.tn.opt.l1.borderHorizontal+'px; border-bottom-width:'+G.tn.opt.l1.borderVertical+'px; border-left-width:'+G.tn.opt.l1.borderHorizontal+'px;}'+'\n';
7763      s+=s1+'.nGY2GThumbnail_lN { border-top-width:'+G.tn.opt.lN.borderVertical+'px; border-right-width:'+G.tn.opt.lN.borderHorizontal+'px; border-bottom-width:'+G.tn.opt.lN.borderVertical+'px; border-left-width:'+G.tn.opt.lN.borderHorizontal+'px;}'+'\n';
7764      s+=s1+'.nGY2GThumbnailStack { background:'+c.stackBackground+'; }'+'\n';
7765      // s+=s1+'.nGY2GThumbnailImage { background:'+cs.thumbnail.background+'; background-image:'+cs.thumbnail.backgroundImage+'; }'+'\n';
7766      s+=s1+'.nGY2TnImgBack { background:'+c.background+'; background-image:'+c.backgroundImage+'; }'+'\n';
7767      s+=s1+'.nGY2GThumbnailAlbumUp { background:'+c.background+'; background-image:'+c.backgroundImage+'; color:'+cs.thumbnail.titleColor+'; }'+'\n';
7768      s+=s1+'.nGY2GThumbnailIconsFullThumbnail { color:'+c.titleColor+'; }\n';
7769      s+=s1+'.nGY2GThumbnailLabel { background:'+c.labelBackground+'; opacity:'+c.labelOpacity+'; }'+'\n';
7770      s+=s1+'.nGY2GThumbnailImageTitle  { color:'+c.titleColor+'; background-color:'+c.titleBgColor+'; '+(c.titleShadow =='' ? '': 'Text-Shadow:'+c.titleShadow+';')+' }'+'\n';
7771      s+=s1+'.nGY2GThumbnailAlbumTitle { color:'+c.titleColor+'; background-color:'+c.titleBgColor+'; '+(c.titleShadow =='' ? '': 'Text-Shadow:'+c.titleShadow+';')+' }'+'\n';
7772      s+=s1+'.nGY2GThumbnailDescription { color:'+c.descriptionColor+'; background-color:'+c.descriptionBgColor+'; '+(c.descriptionShadow =='' ? '': 'Text-Shadow:'+c.descriptionShadow+';')+' }'+'\n';
7773
7774      // thumbnails - icons
7775      var c = cs.thumbnailIcon;
7776      s+=s1+'.nGY2GThumbnailIcons { padding:'+c.padding+'; }\n';
7777      s+=s1+'.nGY2GThumbnailIcon { color:'+c.color+'; '+(c.shadow =='' ? '': 'Text-Shadow:'+c.shadow+';')+' }\n';
7778      s+=s1+'.nGY2GThumbnailIconTextBadge { background-color:'+c.color+'; }\n';
7779      
7780      // gallery pagination -> dot/rectangle based
7781      var c = cs.pagination;
7782      if( G.O.galleryPaginationMode != 'NUMBERS' ) {
7783        s+=s1+'.nGY2paginationDot { border:'+c.shapeBorder+'; background:'+c.shapeColor+';}\n';
7784        s+=s1+'.nGY2paginationDotCurrentPage { border:'+c.shapeBorder+'; background:'+c.shapeSelectedColor+';}\n';
7785        s+=s1+'.nGY2paginationRectangle { border:'+c.shapeBorder+'; background:'+c.shapeColor+';}\n';
7786        s+=s1+'.nGY2paginationRectangleCurrentPage { border:'+c.shapeBorder+'; background:'+c.shapeSelectedColor+';}\n';
7787      } else {
7788        s+=s1+'.nGY2paginationItem { background:'+c.background+'; color:'+c.color+'; border-radius:'+c.borderRadius+'; }\n';
7789        s+=s1+'.nGY2paginationItemCurrentPage { background:'+c.background+'; color:'+c.color+'; border-radius:'+c.borderRadius+'; }\n';
7790        s+=s1+'.nGY2PaginationPrev { background:'+c.background+'; color:'+c.color+'; border-radius:'+c.borderRadius+'; }\n';
7791        s+=s1+'.nGY2PaginationNext { background:'+c.background+'; color:'+c.color+'; border-radius:'+c.borderRadius+'; }\n';
7792        s+=s1+'.nGY2paginationItemCurrentPage { background:'+c.backgroundSelected+'; }\n';
7793      }
7794      
7795      // gallery more button
7796      var c = cs.thumbnail;
7797      // s+=s1+'.nGY2GalleryMoreButtonAnnotation { background:'+c.background+'; border-color:'+c.borderColor+'; border-top-width:'+G.O.thumbnailBorderVertical+'px; border-right-width:'+G.O.thumbnailBorderHorizontal+'px; border-bottom-width:'+G.O.thumbnailBorderVertical+'px; border-left-width:'+G.O.thumbnailBorderHorizontal+'px;}\n';
7798      s+=s1+'.nGY2GalleryMoreButtonAnnotation { background:'+c.background+'; border-color:'+c.borderColor+'; border-top-width: 1px; border-right-width: 1px; border-bottom-width: 1px; border-left-width: 1px;}\n';
7799      s+=s1+'.nGY2GalleryMoreButtonAnnotation  { color:'+c.titleColor+'; '+(c.titleShadow =='' ? '': 'Text-Shadow:'+c.titleShadow)+'; }\n';
7800      
7801      jQuery('head').append('<style id="ngycs_'+G.baseEltID+'">'+s+'</style>');
7802      G.$E.base.addClass(galleryTheme);
7803
7804    };
7805    
7806    // ##### VIEWER COLOR SCHEME #####
7807    function SetViewerTheme( ) {
7808
7809      if( G.VOM.viewerTheme != '' ) {
7810        G.VOM.$baseCont.addClass(G.VOM.viewerTheme);
7811        return;
7812      }
7813
7814      if( typeof G.O.colorSchemeViewer  !== 'undefined' ) {
7815        G.O.viewerTheme = G.O.colorSchemeViewer;
7816      }
7817
7818      var cs=null;
7819      switch(toType(G.O.viewerTheme)) {
7820        case 'object':    // user custom color scheme object 
7821          cs = G.viewerTheme_dark;
7822          jQuery.extend(true, cs, G.O.viewerTheme);
7823          G.VOM.viewerTheme = 'nanogallery_viewertheme_custom_' + G.baseEltID;
7824          break;
7825        case 'string':    // name of an internal defined color scheme
7826          switch( G.O.viewerTheme ) {
7827            case 'none':
7828              return;
7829              break;
7830            case 'light':
7831              cs = G.viewerTheme_light;
7832              G.VOM.viewerTheme = 'nanogallery_viewertheme_light_' + G.baseEltID;
7833              break;
7834            case 'dark':
7835            case 'default':
7836              cs = G.viewerTheme_dark;
7837              G.VOM.viewerTheme = 'nanogallery_viewertheme_dark_' + G.baseEltID;
7838              break;
7839          }
7840          break;
7841        default:
7842          NanoAlert(G, 'Error in viewerTheme parameter.');
7843          return;
7844      }
7845
7846      var s1 = '.' + G.VOM.viewerTheme + ' ';
7847      var s = s1 + '.nGY2Viewer { background:' + cs.background + '; }'+'\n';
7848      s += s1 + '.nGY2Viewer .toolbarBackground { background:' + cs.barBackground + '; }'+'\n';
7849      s += s1 + '.nGY2Viewer .toolbar { border:' + cs.barBorder + '; color:' + cs.barColor + '; }'+'\n';
7850      s += s1 + '.nGY2Viewer .toolbar .previousButton:after { color:' + cs.barColor + '; }'+'\n';
7851      s += s1 + '.nGY2Viewer .toolbar .nextButton:after { color:' + cs.barColor + '; }'+'\n';
7852      s += s1 + '.nGY2Viewer .toolbar .closeButton:after { color:' + cs.barColor + '; }'+'\n';
7853      s += s1 + '.nGY2Viewer .toolbar .label .title { color:' + cs.barColor + '; }'+'\n';
7854      s += s1 + '.nGY2Viewer .toolbar .label .description { color:' + cs.barDescriptionColor + '; }'+'\n';
7855      jQuery('head').append('<style>' + s + '</style>');
7856      G.VOM.$baseCont.addClass(G.VOM.viewerTheme);
7857    };
7858
7859    
7860    
7861    /** @function SetPolyFills */
7862    function SetPolyFills() {
7863
7864      // POLYFILL FOR BIND function --> for older Safari mobile
7865      // found on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility
7866      if (!Function.prototype.bind) {
7867        Function.prototype.bind = function (oThis) {
7868          if (typeof this !== "function") {
7869            // closest thing possible to the ECMAScript 5
7870            // internal IsCallable function
7871            throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
7872          }
7873
7874          var aArgs = Array.prototype.slice.call(arguments, 1), 
7875              fToBind = this, 
7876              fNOP = function () {},
7877              fBound = function () {
7878                return fToBind.apply(this instanceof fNOP && oThis
7879                       ? this
7880                       : oThis,
7881                       aArgs.concat(Array.prototype.slice.call(arguments)));
7882              };
7883
7884          fNOP.prototype = this.prototype;
7885          fBound.prototype = new fNOP();
7886
7887          return fBound;
7888        };
7889      }
7890
7891      // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
7892      // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
7893      // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
7894      // MIT license
7895      (function() {
7896        var lastTime = 0;
7897        var vendors = ['ms', 'moz', 'webkit', 'o'];
7898        for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
7899          window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
7900          window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
7901        }
7902        if (!window.requestAnimationFrame)
7903          window.requestAnimationFrame = function(callback, element) {
7904            var currTime = new Date().getTime();
7905            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
7906            var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);
7907            lastTime = currTime + timeToCall;
7908            return id;
7909          };
7910       
7911        if (!window.cancelAnimationFrame)
7912          window.cancelAnimationFrame = function(id) {
7913            clearTimeout(id);
7914          };
7915      }());
7916      
7917      // array.ngy2removeIf -> removes items from array base on a function's result
7918      Array.prototype.ngy2removeIf = function(callback) {
7919        var i = this.length;
7920        while (i--) {
7921          if (callback(this[i], i)) {
7922            this.splice(i, 1);
7923          }
7924        }
7925      };  
7926
7927      // IE11 for startsWith 
7928      // thanks to @lichtamberg - https://github.com/lichtamberg
7929      if (!String.prototype.startsWith) {
7930        String.prototype.startsWith = function(searchString, position) {
7931          position = position || 0;
7932          return this.indexOf(searchString, position) === position;
7933        };
7934      }
7935      
7936    }
7937    
7938    
7939    // Gallery clicked or toolbar touched -> retrieve & execute action
7940    function GalleryClicked(e) {
7941    
7942      var r = GalleryEventRetrieveElementl(e, false);
7943
7944      if( r.GOMidx == -1 ) { return 'exit'; }
7945      
7946      var idx = G.GOM.items[r.GOMidx].thumbnailIdx;
7947      if( G.GOM.slider.hostIdx == r.GOMidx ) {
7948        idx = G.GOM.items[G.GOM.slider.currentIdx].thumbnailIdx;
7949      }
7950      switch( r.action ) {
7951        case 'OPEN':
7952          ThumbnailOpen(idx, false);
7953          return 'exit';
7954          break;
7955        case 'DISPLAY':
7956          // used the display icon (ignore if selection mode)
7957          ThumbnailOpen(idx, true);
7958          return 'exit';
7959          break;
7960        case 'TOGGLESELECT':
7961          ThumbnailSelectionToggle(idx);
7962          return 'exit';
7963          break;
7964        case 'SHARE':
7965          PopupShare(idx);
7966          return 'exit';
7967          break;
7968        case 'DOWNLOAD':
7969          DownloadImage(idx);
7970          return 'exit';
7971          break;
7972        case 'INFO':
7973          ItemDisplayInfo(G.I[idx]);
7974          return 'exit';
7975          break;
7976        case 'SHOPPINGCART':
7977          AddToCart(idx, 'gallery');
7978          return 'exit';
7979          break;
7980        default:
7981          // all other actions (custom1..10, or anything else)
7982          var fu = G.O.fnThumbnailToolCustAction;
7983          if( fu !== null ) {
7984            typeof fu == 'function' ? fu(r.action, G.I[idx]) : window[fu](r.action, G.I[idx]);
7985          }
7986          break;
7987      }
7988    }
7989
7990    // Download an image
7991    function DownloadImage(idx) {
7992      if( G.I[idx].mediaKind != 'img' ) { return; }
7993
7994      
7995      var url = G.I[idx].src;
7996
7997      if( G.I[idx].downloadURL != undefined && G.I[idx].downloadURL != '' ) {
7998        url = G.I[idx].downloadURL;
7999      }
8000      
8001      var a = document.createElement('a');
8002      a.href = url;
8003      // a.download = url.split('.').pop();
8004      a.download = url.split('/').pop();
8005      a.target = '_blank';
8006      a.style.display = 'none';
8007      document.body.appendChild(a);
8008      a.click();
8009      document.body.removeChild(a);      
8010      
8011    }
8012    
8013    // add one image to the shopping cart
8014    function AddToCart( idx, source ) {
8015      // increment quantity if already in shopping cart
8016      var found=false;
8017      for( var i=0; i<G.shoppingCart.length; i++ ) {
8018        if( G.shoppingCart[i].idx == idx ) {
8019          G.shoppingCart[i].qty++;
8020          ThumbnailBuildToolbarOneCartUpdate( G.I[idx] );
8021          
8022          var fu = G.O.fnShoppingCartUpdated;
8023          if( fu !== null ) {
8024            typeof fu == 'function' ? fu(G.shoppingCart, G.I[idx], source) : window[fu](G.shoppingCart, G.I[idx], source);
8025          }
8026          TriggerCustomEvent('shoppingCartUpdated');
8027          return;
8028        }
8029      }
8030      
8031      // add to shopping cart
8032      if( !found) {
8033        G.shoppingCart.push( { idx:idx, ID:G.I[idx].GetID(), qty:1} );
8034        ThumbnailBuildToolbarOneCartUpdate(G.I[idx]);
8035
8036        var fu=G.O.fnShoppingCartUpdated;
8037        if( fu !== null ) {
8038          typeof fu == 'function' ? fu(G.shoppingCart, G.I[idx], source) : window[fu](G.shoppingCart, G.I[idx], source);
8039        }
8040        TriggerCustomEvent('shoppingCartUpdated');
8041      }
8042    }
8043    
8044
8045    // All thumbnails are set to unselected
8046    function ThumbnailSelectionClear() {
8047      G.GOM.nbSelected = 0;
8048      for( var i = 0, nbTn = G.GOM.items.length; i < nbTn ; i++ ) {
8049        var item = G .I[G.GOM.items[i].thumbnailIdx];
8050        if( item.selected ) {
8051          item.selected = false;
8052          var fu = G.O.fnThumbnailSelection;
8053          if( fu !== null ) {
8054            typeof fu == 'function' ? fu(item.$elt, item, G.I) : window[fu](item.$elt, item, G.I);
8055          }
8056        }
8057        item.selected = false;
8058      }
8059    }
8060    
8061    function ThumbnailSelectionToggle( idx ){
8062      var item = G.I[idx];
8063      if( item.selected === true ) {
8064        ThumbnailSelectionSet(item, false);
8065        G.GOM.nbSelected--;
8066        TriggerCustomEvent('itemUnSelected');
8067      }
8068      else {
8069        ThumbnailSelectionSet(item, true);
8070        G.GOM.nbSelected++;
8071        TriggerCustomEvent('itemSelected');
8072      }
8073    }
8074    
8075    
8076    // this replaces ThumbnailSelection()
8077    function ThumbnailSelectionSet(item, selected ){
8078      
8079      item.selected = selected;
8080      
8081      ThumbnailSelectionSetIcon( item );
8082      
8083      // called when the selection status of an item changed
8084      var fu=G.O.fnThumbnailSelection;
8085      if( fu !== null ) {
8086        typeof fu == 'function' ? fu(item.$elt, item, G.I) : window[fu](item.$elt, item, G.I);
8087      }
8088    
8089    }
8090    
8091    function ThumbnailSelectionSetIcon( item ) {
8092      if( item.$elt == null ) {
8093        // thumbnail is not built
8094        return;
8095      }
8096      var $sub = item.$getElt('.nGY2GThumbnail');
8097      var $icon = item.$getElt('.nGY2GThumbnailIconImageSelect');
8098      if( item.selected === true) {
8099        $sub.addClass('nGY2GThumbnailSubSelected');
8100        $icon.addClass('nGY2ThumbnailSelected');
8101        $icon.removeClass('nGY2ThumbnailUnselected');
8102        $icon.html(G.O.icons.thumbnailSelected);
8103      }
8104      else {
8105        $sub.removeClass('nGY2GThumbnailSubSelected');
8106        $icon.removeClass('nGY2ThumbnailSelected');
8107        $icon.addClass('nGY2ThumbnailUnselected');
8108        $icon.html(G.O.icons.thumbnailUnselected);
8109      }
8110    }
8111    
8112    
8113    // display a modal popup for sharing image/album
8114    function PopupShare(idx) {
8115    
8116      // SEE SAMPLES: https://gist.github.com/chrisjlee/5196139
8117      // https://github.com/Julienh/Sharrre
8118    
8119      var item=G.I[idx];
8120
8121      var currentURL=document.location.protocol + '//' + document.location.hostname + document.location.pathname;
8122      var newLocationHash = '#nanogallery/' + G.baseEltID + '/';
8123      if( item.kind == 'image' ) {
8124        newLocationHash += item.albumID + '/' + item.GetID();
8125      }
8126      else {
8127        newLocationHash += item.GetID();
8128      }
8129    
8130      var content = '<br><br>';
8131      content += '<div class="nGY2PopupOneItem" style="text-align:center;" data-share="facebook">'   + G.O.icons.shareFacebook +   '</div>';
8132      content += '<div class="nGY2PopupOneItem" style="text-align:center;" data-share="pinterest">'  + G.O.icons.sharePinterest +  '</div>';
8133      content += '<div class="nGY2PopupOneItem" style="text-align:center;" data-share="tumblr">'     + G.O.icons.shareTumblr +     '</div>';
8134      content += '<div class="nGY2PopupOneItem" style="text-align:center;" data-share="twitter">'    + G.O.icons.shareTwitter +    '</div>';
8135      // content += '<div class="nGY2PopupOneItem" style="text-align:center;" data-share="googleplus">' + G.O.icons.shareGooglePlus + '</div>';
8136      content += '<div class="nGY2PopupOneItem" style="text-align:center;" data-share="vk">'         + G.O.icons.shareVK +         '</div>';
8137      content += '<div class="nGY2PopupOneItem" style="text-align:center;" data-share="mail">'       + G.O.icons.shareMail +       '</div>';
8138      content += '<div class="nGY2PopupOneItem" style="text-align:center;"></div>';
8139      content += '<input class="nGY2PopupOneItemText" readonly type="text" value="' + currentURL+newLocationHash + '" style="width:100%;text-align:center;">';
8140      content += '<br>';
8141
8142      currentURL = encodeURIComponent(document.location.protocol + '//' + document.location.hostname + document.location.pathname + newLocationHash);
8143
8144      var currentTitle = item.title;
8145      var currentTn = item.thumbImg().src;
8146      
8147      
8148      Popup('nanogallery2 - share to:', content, 'Center');
8149      
8150      G.popup.$elt.find('.nGY2PopupOneItem').on('click', function(e) {
8151        e.stopPropagation();
8152        
8153        var shareURL = '';
8154        var found = true;
8155        switch(jQuery(this).attr('data-share').toUpperCase()) {
8156          case 'FACEBOOK':
8157            // <a name="fb_share" type="button" href="http://www.facebook.com/sharer.php?u={$url}&media={$imgPath}&description={$desc}" class="joinFB">Share Your Advertise</a>
8158            //window.open("https://www.facebook.com/sharer.php?u="+currentURL,"","height=368,width=600,left=100,top=100,menubar=0");
8159            shareURL = 'https://www.facebook.com/sharer.php?u=' + currentURL;
8160            break;
8161          case 'VK':
8162            shareURL = 'http://vk.com/share.php?url=' + currentURL;
8163            break;
8164          case 'GOOGLEPLUS':
8165            shareURL = "https://plus.google.com/share?url=" + currentURL;
8166            break;
8167          case 'TWITTER':
8168            // shareURL="https://twitter.com/share?url="+currentURL+"&text="+currentTitle;
8169            shareURL = 'https://twitter.com/intent/tweet?text=' + currentTitle + 'url=' + currentURL;
8170            break;
8171          case 'PINTEREST':
8172            // shareURL='https://pinterest.com/pin/create/bookmarklet/?media='+currentTn+'&url='+currentURL+'&description='+currentTitle;
8173            shareURL = 'https://pinterest.com/pin/create/button/?media=' + currentTn + '&url=' + currentURL + '&description=' + currentTitle;
8174            break;
8175          case 'TUMBLR':
8176            //shareURL='https://www.tumblr.com/widgets/share/tool/preview?caption=<strong>'+currentTitle+'</strong>&tags=nanogallery2&url='+currentURL+'&shareSource=legacy&posttype=photo&content='+currentTn+'&clickthroughUrl='+currentURL;
8177            shareURL = 'http://www.tumblr.com/share/link?url=' + currentURL + '&name=' + currentTitle;
8178            break;
8179          case 'MAIL':
8180            shareURL = 'mailto:?subject=' + currentTitle + '&body=' + currentURL;
8181            break;
8182          default:
8183            found = false;
8184            break;
8185        }
8186        
8187        if( found ) {
8188          window.open(shareURL, "" , "height=550,width=500,left=100,top=100,menubar=0" );
8189          G.popup.close();
8190          // $popup.remove();
8191        }
8192        
8193      });
8194    }
8195    
8196    // build a modal popup
8197    function Popup(title, content, align) {
8198      var pp =  '<div class="nGY2Popup" style="opacity:0;"><div class="nGY2PopupContent' + align + '">';
8199      pp    +=  '<div class="nGY2PopupCloseButton" style="font-size:0.9em;">' + G.O.icons.buttonClose + '</div>';
8200      pp    +=  '<div class="nGY2PopupTitle">' + title + '</div>';
8201      pp    +=  content;
8202      pp    +=  '</div></div>';
8203      
8204      G.popup.$elt = jQuery(pp).appendTo('body');
8205      setElementOnTop( G.VOM.$viewer, G.popup.$elt);
8206      
8207      G.popup.isDisplayed = true;
8208      
8209      var tweenable = new NGTweenable();
8210      tweenable.tween({
8211        from:       { o: 0, y: 100 },
8212        to:         { o: 1, y: 0 },
8213        easing:     'easeInOutSine',
8214        duration:   250,
8215        step:       function (state, att) {
8216          G.popup.$elt[0].style.opacity = state.o;
8217          G.popup.$elt[0].style[G.CSStransformName] = 'translateY(' + (state.y) + 'px)';
8218        }
8219      });
8220      
8221      G.popup.$elt.find('.nGY2PopupCloseButton').on('click', function(e) {
8222        e.stopPropagation();
8223        G.popup.close();
8224      });
8225      
8226    }
8227
8228
8229    function GalleryMouseEnter(e) {
8230      if( !G.VOM.viewerDisplayed && G.GOM.albumIdx != -1 ) {
8231        var r = GalleryEventRetrieveElementl(e, true);
8232        // if( r.action == 'OPEN' && r.GOMidx != -1 ) {
8233        if( r.GOMidx != -1 ) {
8234          // var target = e.target || e.srcElement;
8235          // if( target.getAttribute('class') != 'nGY2GThumbnail' ) { return; }
8236          ThumbnailHover(r.GOMidx);
8237        }
8238      }
8239    }
8240    
8241    function GalleryMouseLeave(e) {
8242      if( !G.VOM.viewerDisplayed && G.GOM.albumIdx != -1 ) {
8243        var r = GalleryEventRetrieveElementl(e, true);
8244        if( r.GOMidx != -1 ) {
8245          // var target = e.target || e.srcElement;
8246          // if( target.getAttribute('class') != 'nGY2GThumbnail' ) { return; }
8247          ThumbnailHoverOut(r.GOMidx);
8248        }
8249      }
8250    }
8251    
8252    function GalleryEventRetrieveElementl( e, ignoreSubItems ) {
8253      var r = { action: 'NONE', GOMidx: -1 };
8254      
8255      if( e == undefined ) {
8256        return r;
8257      }
8258      var target = e.target || e.srcElement;
8259      while( target != G.$E.conTnParent[0] ) {       // loop element parent up to find the thumbnail element
8260        if( jQuery(target).hasClass('nGY2GThumbnail') ) {
8261          if( r.action == 'NONE' ) {
8262            r.action = 'OPEN';
8263          }
8264          r.GOMidx = jQuery(target).data('index');
8265          return r;
8266        }
8267        // if( !ignoreSubItems && jQuery(target).hasClass('nGY2GThumbnailIcon') ) {
8268        if( !ignoreSubItems ) {
8269          var a = jQuery(target).data('ngy2action');
8270          if( a != '' && a != undefined ) {
8271            r.action = a;
8272          }
8273        }
8274        if( target.parentNode == null ) {
8275          return r;
8276        }
8277        target = target.parentNode;
8278      }
8279      return r;
8280    }
8281    
8282
8283    // OPEN ONE THUMBNAIL
8284    function ThumbnailOpen( idx, ignoreSelected ) {
8285      var item = G.I[idx];
8286      
8287      G.GOM.albumIdxLoading = idx;      // store idx -> may be used to display loader on album thumbnail
8288
8289      var fu = G.O.fnThumbnailClicked;
8290      if( fu !== null ) {
8291        typeof fu == 'function' ? fu(item.$elt, item) : window[fu](item.$elt, item);
8292      }
8293      
8294      // open URL
8295      if( item.destinationURL !== undefined && item.destinationURL.length > 0 ) {
8296        window.location = item.destinationURL;
8297        return;
8298      }
8299
8300      switch( item.kind ) {
8301        case 'image':
8302          if( ignoreSelected === false && G.GOM.nbSelected > 0 ) {
8303            ThumbnailSelectionToggle(idx);
8304          }
8305          else {
8306            // display image
8307            DisplayPhotoIdx( idx );
8308          }
8309          break;
8310        case 'album':
8311          if( ignoreSelected === false && G.GOM.nbSelected > 0 ) {
8312            ThumbnailSelectionToggle( idx );
8313          }
8314          else {
8315            if( G.O.thumbnailAlbumDisplayImage && idx != 0 ) {
8316              // display album content in lightbox
8317              DisplayFirstMediaInAlbum( idx );
8318              return;
8319            }
8320            else {
8321              // display album content in gallery
8322              DisplayAlbum('-1', item.GetID());
8323            }
8324          }
8325          break;
8326        case 'albumUp':
8327          var parent = NGY2Item.Get(G, item.albumID);
8328          DisplayAlbum('-1', parent.albumID);
8329          break;
8330      }
8331    }
8332    
8333    function DisplayFirstMediaInAlbum( albumIdx ) {
8334      if( G.O.debugMode ) { console.log('#DisplayFirstPhotoInAlbum : '+  albumIdx); }
8335
8336      var item = G.I[albumIdx];
8337      
8338      var l = G.I.length;
8339      for( var i = 0; i < l; i++ ) {
8340        if( G.I[i].albumID == item.GetID() ) {
8341          DisplayPhotoIdx( i );
8342          return;
8343        }
8344      }
8345      
8346      // load album content
8347      AlbumGetContent( item.GetID(), DisplayFirstMediaInAlbum, albumIdx, null );
8348      
8349    }
8350    
8351
8352    // Open link to original image (new window)
8353    function OpenOriginal( item ) {
8354      switch( G.O.kind ) {
8355        case 'flickr':
8356          var sU = 'https://www.flickr.com/photos/' + G.O.userID + '/' + item.GetID();
8357          if( item.albumID != '0' ) {
8358            sU += '/in/album-' + item.albumID + '/';
8359          }
8360          window.open(sU, '_blank');
8361          break;
8362        case 'picasa':
8363        case 'google':
8364        case 'google2':
8365          // no more working since Google changed the access to Google Photos in 2017
8366          // var sU='https://plus.google.com/photos/'+G.O.userID+'/albums/'+item.albumID+'/'+item.GetID();
8367          // window.open(sU,'_blank');
8368          // break;
8369        default:
8370          var sU = item.responsiveURL();
8371          window.open(sU, '_blank');
8372          break;
8373      }
8374    }
8375    
8376    // ########################################################
8377    // DISPLAY ONE MEDIA
8378    // with internal or external viewer
8379    // ########################################################
8380    function DisplayPhotoIdx( ngy2ItemIdx ) {
8381
8382      if( !G.O.thumbnailOpenInLightox ) { return; }
8383
8384      if( G.O.thumbnailOpenOriginal ) {
8385        // Open link to original image
8386        OpenOriginal( G.I[ngy2ItemIdx] );
8387        return;
8388      }
8389        
8390      var items = [];
8391//      G.VOM.currItemIdx = 0;
8392      G.VOM.content.current.vIdx = 0;
8393      G.VOM.items = [];
8394      G.VOM.albumID = G.I[ngy2ItemIdx].albumID;
8395      
8396      var vimg = new VImg(ngy2ItemIdx);
8397      G.VOM.items.push(vimg);
8398      items.push(G.I[ngy2ItemIdx]);
8399      //TODO -> danger? -> pourquoi reconstruire la liste si déjà ouvert (back/forward)     
8400      var l = G.I.length;
8401      for( let idx = ngy2ItemIdx+1; idx < l ; idx++) {
8402        let item = G.I[idx];
8403        if( item.kind == 'image' && item.isToDisplay(G.VOM.albumID) && item.destinationURL == '' ) {
8404          let vimg = new VImg(idx);
8405          G.VOM.items.push(vimg);
8406          items.push(item);
8407        }
8408      }
8409      var last = G.VOM.items.length;
8410      var cnt = 1;
8411      for( let idx = 0; idx < ngy2ItemIdx ; idx++) {
8412        let item = G.I[idx];
8413        if( item.kind == 'image' && item.isToDisplay(G.VOM.albumID) && item.destinationURL == '' ) {
8414          let vimg = new VImg(idx);
8415          vimg.mediaNumber = cnt;
8416          G.VOM.items.push(vimg);
8417          items.push(item);
8418          cnt++;
8419        }
8420      }
8421      for( let i = 0; i < last; i++ ) {
8422        G.VOM.items[i].mediaNumber = cnt;
8423        cnt++;
8424      }
8425    
8426      // opens media with external viewer
8427      var fu = G.O.fnThumbnailOpen;
8428      if( fu !== null ) {
8429        typeof fu == 'function' ? fu(items) : window[fu](items);
8430        return;
8431      }
8432    
8433      // use internal viewer
8434      if( !G.VOM.viewerDisplayed ) {
8435        // build viewer and display
8436        LightboxOpen();
8437      }
8438      else {
8439        // viewer already displayed -> display new media in current viewer
8440        G.VOM.content.current.$media.empty();
8441        let item = G.VOM.content.current.NGY2Item();
8442        var spreloader = '<div class="nGY2ViewerMediaLoaderDisplayed"></div>';
8443        if( item.mediaKind == 'img' && item.imageWidth != 0 && item.imageHeight != 0 ) {
8444          spreloader = '<div class="nGY2ViewerMediaLoaderHidden"></div>';
8445        }
8446        G.VOM.content.current.$media.append( spreloader + item.mediaMarkup);
8447        ViewerSetMediaVisibility(G.VOM.content.next, 0);
8448        ViewerSetMediaVisibility(G.VOM.content.previous, 0);
8449        if( item.mediaKind == 'img' ) {
8450          G.VOM.ImageLoader.loadImage(VieweImgSizeRetrieved, item);
8451        }
8452        // G.VOM.$mediaCurrent.css({ opacity:0 }).attr('src','');
8453        // G.VOM.ImageLoader.loadImage(VieweImgSizeRetrieved, G.VOM.NGY2Item(0));
8454        // G.VOM.$mediaCurrent.children().eq(0).attr('src',G.emptyGif).attr('src', G.VOM.NGY2Item(0).responsiveURL());
8455        // LightboxDisplay(0, '');
8456        LightboxDisplay('');
8457      }
8458    }
8459    
8460    function ViewerZoomStart() {
8461      if( G.O.viewerZoom && !G.VOM.viewerMediaIsChanged ) {
8462        var item = G.VOM.content.current.NGY2Item();
8463        if( item.mediaKind == 'img' && item.imageHeight > 0 && item.imageWidth > 0 ) {
8464          if( G.VOM.zoom.isZooming === false ) {
8465            // default zoom
8466            G.VOM.zoom.userFactor = 1;
8467            G.VOM.zoom.isZooming = true;
8468          }
8469          return true;
8470        }
8471      }
8472      return false;
8473    }
8474          
8475    function ViewerZoomIn( zoomIn ) {
8476    if( zoomIn ) {
8477        // zoom in
8478        G.VOM.zoom.userFactor += 0.1;
8479        ViewerZoomMax();
8480      }
8481      else {
8482        // zoom out
8483        G.VOM.zoom.userFactor -= 0.1;
8484        ViewerZoomMin();
8485      }
8486      ViewerMediaSetPosAndZoom();
8487    }
8488    
8489    function ViewerZoomMax() {
8490      if( G.VOM.zoom.userFactor > 3 ) {
8491        G.VOM.zoom.userFactor = 3;
8492      }
8493    }
8494    function ViewerZoomMin() {
8495    
8496      if( G.VOM.zoom.userFactor < 0.2 ) {
8497        G.VOM.zoom.userFactor = 0.2;
8498      }
8499    }
8500    
8501    
8502    
8503    // Set position and size of all 3 media containers
8504    function ViewerMediaSetPosAndZoom() {
8505    
8506      if( !G.VOM.zoom.isZooming ) {
8507        G.VOM.zoom.userFactor = 1;
8508      }
8509      // window.ng_draf( function() {
8510        ViewerMediaSetPosAndZoomOne( G.VOM.content.current, true );
8511        ViewerMediaSetPosAndZoomOne( G.VOM.content.previous, false );
8512        ViewerMediaSetPosAndZoomOne( G.VOM.content.next, false );
8513      // });
8514    }
8515    
8516
8517    
8518    // Media which is not IMG -> center and set size
8519    function ViewerMediaCenterNotImg( $mediaContainer ) {
8520      var $media = $mediaContainer.children().eq(1);
8521			var h = 90;
8522			if( G.O.viewerGallery != 'none' ) { h -= 10; }
8523			if( G.O.viewerToolbar.display != 'none' ) { h -= 10; }
8524      $media.css( {'height': h+'%' });
8525      $media.css( {'width':  '90%' });
8526      $media[0].style[G.CSStransformName] = 'translate(0px, "50%") ';
8527    }
8528    
8529    // Set position and size of ONE media container
8530    function ViewerMediaSetPosAndZoomOne(content_item, isCurrent ) {
8531
8532      var item = content_item.NGY2Item();
8533      var $img = content_item.$media;
8534      
8535
8536      if( item.mediaKind != 'img' ) {
8537        ViewerMediaCenterNotImg( $img );
8538        return;
8539      }
8540      
8541      if( item.imageHeight == 0 || item.imageWidth == 0 ) { 
8542        // ViewerSetMediaVisibility( item, $img, 0 );
8543        ViewerSetMediaVisibility( content_item, 0 );
8544        return;
8545      }
8546
8547      // part 1: set the image size
8548      var zoomUserFactor = isCurrent == true ? G.VOM.zoom.userFactor : 1;
8549      
8550      var dpr = 1;
8551      if( G.O.viewerImageDisplay == 'bestImageQuality' ) {
8552        dpr = window.devicePixelRatio;
8553      }
8554      
8555      // retrieve the base zoom factor (image fill screen)
8556      var zoomBaseFactorW = (G.VOM.window.lastWidth  - G.VOM.padding.V) / (item.imageWidth  / dpr);
8557      var zoomBaseFactorH = (G.VOM.window.lastHeight - G.VOM.padding.H) / (item.imageHeight / dpr);
8558      var zoomBaseFactor = Math.min(zoomBaseFactorW, zoomBaseFactorH);
8559      if( zoomBaseFactor > 1 && G.O.viewerImageDisplay != 'upscale' ) {
8560        // no upscale
8561        zoomBaseFactor = 1;
8562      }
8563
8564      var imageCurrentHeight = (item.imageHeight / dpr) * zoomUserFactor * zoomBaseFactor;
8565      var imageCurrentWidth  = (item.imageWidth / dpr)  * zoomUserFactor * zoomBaseFactor;
8566      $img.children().eq(1).css( {'height': imageCurrentHeight });
8567      $img.children().eq(1).css( {'width':  imageCurrentWidth  });
8568
8569      // retrieve posX/Y to center image
8570      var posX = 0;
8571      if( imageCurrentWidth > G.VOM.window.lastWidth ) {
8572        posX = -(imageCurrentWidth - G.VOM.window.lastWidth)/2;
8573      }
8574      var posY = 0;
8575      // if( imageCurrentHeight > G.VOM.window.lastHeight ) {
8576      //   posY = ( imageCurrentHeight - G.VOM.window.lastHeight ) / 2;
8577      // }
8578      // posY = 0;   // actually, it seems that the image is always centered vertically -> so no need to to anything
8579      
8580      // Part 2: set the X/Y position (for zoom/pan)
8581      if( isCurrent ) {
8582        if( !G.VOM.zoom.isZooming ) {
8583          G.VOM.panPosX = 0;
8584          G.VOM.panPosY = 0;
8585        }
8586        G.VOM.zoom.posX = posX;
8587        G.VOM.zoom.posY = posY;
8588        ViewerImagePanSetPosition(G.VOM.panPosX, G.VOM.panPosY, $img, false);
8589      }
8590      // else {
8591        //$img[0].style[G.CSStransformName]= 'translate3D('+ posX+'px, '+ posY+'px, 0) ';
8592      // }
8593      else {
8594        // set the pan position of each media container
8595        ViewerMediaPanX( G.VOM.swipePosX );
8596        $img.children().eq(1)[0].style[G.CSStransformName]= 'translate(0px, 0px) rotate('+ item.rotationAngle +'deg)';
8597      }
8598      
8599    }
8600
8601    // position the image depending on the zoom factor and the pan X/Y position
8602    // IMG is the only media kind supporting zoom/pan
8603    function ViewerImagePanSetPosition(posX, posY, imageContainer, savePosition ) {
8604      if( savePosition ) {
8605        G.VOM.panPosX = posX;
8606        G.VOM.panPosY = posY;
8607      }
8608
8609      posX += G.VOM.zoom.posX;
8610      posY += G.VOM.zoom.posY;
8611      
8612      // imageContainer.children().eq(1)[0].style[G.CSStransformName]= 'translate('+ posX + 'px, '+ posY + 'px)';
8613      imageContainer.children().eq(1)[0].style[G.CSStransformName]= 'translate('+ posX + 'px, '+ posY + 'px) rotate('+ G.VOM.content.current.NGY2Item().rotationAngle +'deg)';
8614
8615
8616    }
8617    
8618
8619    // LIGHTBOX
8620    // display media with internal viewer
8621    function LightboxOpen( idx ) {
8622
8623      // G.VOM.viewerDisplayed = true;
8624      G.GOM.firstDisplay = false;
8625      
8626      // remove scrollbar and add right margin with same width as the scrollbar to avoid page reflow
8627      jQuery('head').append('<style id="nGY2_body_scrollbar_style" type="text/css">.nGY2_body_scrollbar{margin-right: ' + (window.innerWidth - document.documentElement.clientWidth) + 'px;}</style>');
8628      jQuery("body").addClass("nGY2_body_scrollbar");
8629        
8630
8631      G.VOM.$baseCont = jQuery('<div  class="nGY2 nGY2ViewerContainer" style="opacity:1"></div>').appendTo('body');
8632      
8633      SetViewerTheme();
8634
8635      G.VOM.$viewer = jQuery('<div class="nGY2Viewer" style="opacity:0" itemscope itemtype="http://schema.org/ImageObject"></div>').appendTo( G.VOM.$baseCont );
8636      G.VOM.$viewer.css({ msTouchAction: 'none', touchAction: 'none' });            // avoid pinch zoom
8637
8638      if( idx == undefined ) {
8639        G.VOM.content.current.vIdx = 0;
8640      }
8641      else {
8642        G.VOM.content.current.vIdx = idx;
8643      }
8644      G.VOM.content.previous.vIdx = G.VOM.IdxNext();
8645      G.VOM.content.next.vIdx = G.VOM.IdxPrevious();   
8646
8647      var sMedia = '<div class="nGY2ViewerMediaPan"><div class="nGY2ViewerMediaLoaderDisplayed"></div>' + G.VOM.content.previous.NGY2Item().mediaMarkup + '</div>';    // previous media
8648      sMedia    += '<div class="nGY2ViewerMediaPan"><div class="nGY2ViewerMediaLoaderDisplayed"></div>' + G.VOM.content.current.NGY2Item().mediaMarkup  + '</div>';    // current media
8649      sMedia    += '<div class="nGY2ViewerMediaPan"><div class="nGY2ViewerMediaLoaderDisplayed"></div>' + G.VOM.content.next.NGY2Item().mediaMarkup  + '</div>';    // next media
8650
8651      var sNav = '';
8652      var iconP = G.O.icons.viewerImgPrevious;
8653      if( iconP != undefined && iconP != '') {
8654        sNav += '<div class="nGY2ViewerAreaPrevious ngy2viewerToolAction" data-ngy2action="previous">' + iconP + '</div>';
8655      }
8656      var iconN = G.O.icons.viewerImgNext;
8657      if( iconN != undefined && iconN != '') {
8658        sNav += '<div class="nGY2ViewerAreaNext ngy2viewerToolAction" data-ngy2action="next">' + iconN + '</div>';
8659      }
8660
8661      G.VOM.$content = jQuery('<div class="nGY2ViewerContent">' + sMedia + sNav + '</div>').appendTo( G.VOM.$viewer );
8662
8663      G.VOM.$buttonLeft = G.VOM.$content.find('.nGY2ViewerAreaPrevious');
8664      G.VOM.$buttonRight = G.VOM.$content.find('.nGY2ViewerAreaNext');
8665
8666      var $mediaPan = G.VOM.$content.find('.nGY2ViewerMediaPan');
8667      G.VOM.content.previous.$media = $mediaPan.eq(0);    // pointer to previous media container
8668      G.VOM.content.current.$media = $mediaPan.eq(1);     // pointer to current media container
8669      G.VOM.content.next.$media = $mediaPan.eq(2);        // pointer to next media container
8670
8671      // position next/previous media
8672      var vP = G.GOM.cache.viewport;
8673      G.VOM.content.previous.$media[0].style[G.CSStransformName] = 'translate(-' + vP.w + 'px, 0px)';
8674      G.VOM.content.next.$media[0].style[G.CSStransformName] = 'translate(' + vP.w + 'px, 0px)';
8675      
8676      
8677      G.VOM.ImageLoader.loadImage( VieweImgSizeRetrieved, G.VOM.content.current.NGY2Item()  );
8678      G.VOM.ImageLoader.loadImage( VieweImgSizeRetrieved, G.VOM.content.previous.NGY2Item() );
8679      G.VOM.ImageLoader.loadImage( VieweImgSizeRetrieved, G.VOM.content.next.NGY2Item()  );
8680      
8681      G.VOM.padding.H = parseInt(G.VOM.$content.css("padding-left")) + parseInt(G.VOM.$content.css("padding-right"));
8682      G.VOM.padding.V = parseInt(G.VOM.$content.css("padding-top")) + parseInt(G.VOM.$content.css("padding-bottom"));
8683      
8684      // build media toolbar container
8685      var vtbBg1 = '';
8686      var vtbBg2 = ' toolbarBackground';
8687      if( G.O.viewerToolbar.fullWidth ) {
8688        vtbBg1 = ' toolbarBackground';
8689        vtbBg2 = '';
8690      }
8691      var vtbAlign = 'text-align:center;';
8692      switch ( G.O.viewerToolbar.align ) {
8693        case 'left':
8694          vtbAlign = 'text-align:left;';
8695          break;
8696        case 'right':
8697          vtbAlign = 'text-align:right;';
8698          break;
8699      }
8700      var sTB = '<div class="toolbarContainer nGEvent' + vtbBg1 + '" style="visibility:' +(G.O.viewerToolbar.display ? "visible" : "hidden")+';'+vtbAlign+'"><div class="toolbar nGEvent' + vtbBg2 + '"></div></div>';
8701      G.VOM.$toolbar = jQuery(sTB).appendTo(G.VOM.$viewer);
8702
8703      if( G.VOM.toolbarMode == 'min' || (G.O.viewerToolbar.autoMinimize > 0 && G.O.viewerToolbar.autoMinimize >= G.GOM.cache.viewport.w) ) {
8704        ViewerToolbarForVisibilityMin();
8705      }
8706      else {
8707        ViewerToolbarForVisibilityStd();
8708      }
8709      
8710      // top-left toolbar
8711      var sTopLeft = '<div class="nGY2ViewerToolsTopLeft nGEvent"><div class="toolbar nGEvent">';
8712      var sTL = G.O.viewerTools.topLeft.split(',');
8713      for( var i = 0, sTLL = sTL.length; i < sTLL; i++) {
8714        sTopLeft += ToolbarAddElt( sTL[i] );
8715      }
8716      sTopLeft += '</div></div>';
8717      G.VOM.$toolbarTL = jQuery(sTopLeft).appendTo(G.VOM.$viewer);
8718
8719      // top-right toolbar
8720      var sTopRight = '<div class="nGY2ViewerToolsTopRight nGEvent"><div class="toolbar nGEvent">';
8721      var sTR = G.O.viewerTools.topRight.split(',');
8722      for( var i = 0, sTRL = sTR.length; i < sTRL; i++) {
8723        sTopRight += ToolbarAddElt( sTR[i] );
8724      }
8725      sTopRight += '</div></div>';
8726      G.VOM.$toolbarTR = jQuery(sTopRight).appendTo(G.VOM.$viewer);
8727
8728      // set the events handler on the toolbars
8729      ViewerToolsOn();
8730
8731      // Go to fullscreen mode
8732      if( ngscreenfull.enabled && G.O.viewerFullscreen ) { 
8733        ngscreenfull.request();
8734        G.VOM.viewerIsFullscreen=true;
8735      }
8736
8737      // Gallery
8738      LightboxGalleryBuild();
8739
8740      setElementOnTop('', G.VOM.$viewer);
8741      ResizeLightbox(true);
8742      G.VOM.gallery.Resize();
8743      G.VOM.timeImgChanged = new Date().getTime();
8744
8745      // viewer display transition
8746      G.VOM.$toolbarTL.css('opacity', 0);
8747      G.VOM.$toolbarTR.css('opacity', 0);
8748      G.VOM.$buttonLeft.css('opacity', 0);
8749      G.VOM.$buttonRight.css('opacity', 0);
8750      if( G.O.viewerGallery != 'none' ) { G.VOM.gallery.$elt.css('opacity', 0); }
8751      G.VOM.$content.css('opacity', 0);
8752      G.VOM.$toolbarTR[0].style[G.CSStransformName] = 'translateY(-40px) ';
8753      G.VOM.$toolbarTL[0].style[G.CSStransformName] = 'translateY(-40px) ';
8754      G.VOM.$buttonLeft[0].style[G.CSStransformName] = 'translateX(-40px) ';
8755      G.VOM.$buttonRight[0].style[G.CSStransformName] = 'translateX(40px) ';
8756
8757      // STEP 1: display main container, including media
8758      new NGTweenable().tween({
8759        from:         { opacity: 0, posY: G.VOM.window.lastHeight*.5 },
8760        to:           { opacity: 1, posY: 0 },
8761        delay:        10,
8762        duration:     450,
8763        easing:       'easeInOutQuint',
8764        step:         function (state) {
8765          // lightbox
8766          G.VOM.$viewer.css('opacity', state.opacity);
8767          G.VOM.$viewer[0].style[G.CSStransformName] = 'translateY(' + (state.posY) + 'px) ';
8768          
8769          // media in lightbox
8770          G.VOM.$content.css('opacity', state.opacity);
8771        }
8772      });
8773      
8774      
8775      // STEP 2: display tools, left/right navigation buttons, gallery
8776      new NGTweenable().tween({
8777        from:         { posY: -40, opacity: 0, scale: 3 },
8778        to:           { posY: 0, opacity: 1, scale: 1 },
8779        delay:        300,
8780        duration:     400,
8781        easing:       'easeInOutQuint',
8782        step:         function (state) {
8783          
8784          // tools
8785          G.VOM.$toolbarTR[0].style[G.CSStransformName] = 'translateY(' + (state.posY) + 'px) ';
8786          G.VOM.$toolbarTL[0].style[G.CSStransformName] = 'translateY(' + (state.posY) + 'px) ';
8787          G.VOM.$buttonLeft[0].style[G.CSStransformName] = 'translateX(' + (state.posY) + 'px) ';
8788          G.VOM.$buttonRight[0].style[G.CSStransformName] = 'translateX(' + (-state.posY) + 'px) ';
8789          
8790          // gallery
8791          if( G.O.viewerGallery != 'none' ) {
8792            G.VOM.gallery.$elt.css({ opacity: state.opacity });
8793            G.VOM.gallery.$elt[0].style[G.CSStransformName] = 'scale('+state.scale+')';
8794          }
8795          
8796        },
8797        finish: function() {
8798          G.VOM.viewerDisplayed = true;
8799          ViewerMediaPanX(0);
8800          ViewerSetEvents();
8801
8802          LightboxDisplay('');
8803          
8804          if( G.O.slideshowAutoStart ) {
8805            G.VOM.playSlideshow = false;
8806            SlideshowToggle();
8807          }
8808          
8809          ViewerToolsUnHide();
8810          LightboxDisplayFinalize('');
8811        }
8812      });
8813
8814
8815      
8816
8817      // stop click propagation on media ==> if the user clicks outside of an media, the viewer is closed
8818      // --> no more supported since v2.0.0
8819      // G.VOM.$viewer.find('img').on('click', function (e) { e.stopPropagation(); });
8820      
8821      
8822      // ViewerMediaPanX(0);
8823      // ViewerSetEvents();
8824
8825      // LightboxDisplay('');
8826      
8827      // if( G.O.slideshowAutoStart ) {
8828        // G.VOM.playSlideshow = false;
8829        // SlideshowToggle();
8830      // }
8831    }
8832    
8833    function ViewerEvents() {
8834      if( !G.VOM.viewerDisplayed || G.VOM.viewerMediaIsChanged ) {
8835      // if( !G.VOM.viewerDisplayed || G.VOM.viewerMediaIsChanged || G.VOM.content.current.NGY2Item().mediaKind != 'img') {
8836        // ignore fired event if viewer not displayed or if currently changed (or if current media not an image)
8837        return false;
8838      }
8839      return true;
8840    }
8841    
8842    // VIEWER - BUILD THE THUMBNAILS GALLERY
8843    function LightboxGalleryBuild() {
8844
8845			G.VOM.gallery.firstDisplay = true;
8846	
8847			if( G.O.viewerGallery != 'none' ) {
8848	
8849				var tw = G.O.viewerGalleryTWidth;
8850				var th = G.O.viewerGalleryTHeight;
8851				var gutter = 2;
8852				
8853				var t = '';
8854				for( var i=0; i< G.VOM.items.length; i++) {
8855					var idx = G.VOM.items[i].ngy2ItemIdx;
8856					var o = G.I[idx];
8857					var src = (o.thumbImg().src).replace(/'/g, "%27");   // replace single quote with %27
8858          src = src.replace(/\\/g, '\\\\');     // single backslashes are replaced by double backslashes
8859					t += '<div class="nGY2VThumbnail" style="width:'+tw+'px;height:'+th+'px;left:'+i*(tw+gutter*2)+'px;background-image: url(&apos;'+src+'&apos;);" data-ngy2_lightbox_thumbnail="true" data-ngy2_idx="' + idx + '" data-ngy2_vidx="' + i + '" ></div>';
8860				}
8861				G.VOM.gallery.gwidth = (tw+2*gutter) * G.VOM.items.length;
8862				G.VOM.gallery.oneTmbWidth = tw+2*gutter;
8863				var tc = "<div class='nGY2VThumbnailContainer' style='height:"+(th+gutter*2)+"px;left:0;width:"+G.VOM.gallery.gwidth+"px;' data-ngy2_lightbox_gallery='true'>" + t + "</div>";
8864				G.VOM.gallery.$elt = jQuery('<div class="nGY2viewerGallery" style="display: inline-block;height:'+(th+gutter*2)+'px;left:0;right:0;">'+ tc +'</div>').appendTo(G.VOM.$viewer);
8865				G.VOM.gallery.$tmbCont = G.VOM.gallery.$elt.find('.nGY2VThumbnailContainer')
8866				
8867				G.VOM.gallery.Resize();
8868				G.VOM.gallery.SetThumbnailActive();
8869      
8870			}
8871    }
8872    
8873
8874    // Lightbox gesture handling
8875    function ViewerSetEvents() {
8876
8877      if( G.VOM.hammertime == null ) {
8878      
8879        G.VOM.hammertime =  new NGHammer.Manager(G.VOM.$baseCont[0],  {
8880          // domEvents: true,
8881          recognizers: [
8882            [NGHammer.Pinch, { enable: true }],
8883            [NGHammer.Pan, { direction: NGHammer.DIRECTION_ALL }]
8884          ]
8885        });
8886     
8887        // PAN
8888        G.VOM.hammertime.on('pan', function(ev) {
8889          if( !ViewerEvents() ) { return; }
8890
8891          
8892          if( G.VOM.panMode == 'off' ) {
8893            // PAN START -> determine the element to pan
8894            if( ev.target.dataset.ngy2_lightbox_thumbnail != undefined || ev.target.dataset.ngy2_lightbox_gallery != undefined ){
8895              G.VOM.panMode = 'gallery';
8896            }
8897            else {
8898              if( G.VOM.zoom.isZooming ) {
8899                G.VOM.panMode = 'zoom';
8900              }
8901              else {
8902                G.VOM.panMode = 'media';
8903              }
8904            }
8905          }
8906
8907          // PAN the determined element
8908          switch( G.VOM.panMode ) {
8909            case 'zoom':
8910              // pan zoomed image
8911              ViewerImagePanSetPosition(G.VOM.panPosX + ev.deltaX, G.VOM.panPosY + ev.deltaY, G.VOM.content.current.$media, false);
8912              G.VOM.toolsHide();
8913              break;
8914              
8915            case 'media':
8916              if( Math.abs(ev.deltaY) > G.VOM.panThreshold && Math.abs(ev.deltaX) < G.VOM.panThreshold && !G.VOM.panXOnly ) {
8917                // pan viewer down/up to close the lightbox
8918                ViewerMediaPanX( 0 );
8919                var dist = 0;
8920                if( ev.deltaY < 0 ) {
8921                  // pan up
8922                  dist = Math.max( ev.deltaY, -200);
8923                }
8924                else {
8925                  // pan down
8926                  dist = Math.min( ev.deltaY, 200);
8927                }
8928                G.VOM.$viewer[0].style[G.CSStransformName] = 'translateY(' + dist + 'px) ';
8929                G.VOM.$viewer.css('opacity', 1-Math.abs(dist)/200/2);
8930              }
8931              else {
8932                // pan media left/right
8933                if( Math.abs(ev.deltaX) > G.VOM.panThreshold ) {
8934                  G.VOM.panXOnly = true;
8935                }
8936                ViewerMediaPanX( ev.deltaX );
8937                G.VOM.$viewer[0].style[G.CSStransformName] = 'translateY(0px)';
8938                G.VOM.$viewer.css('opacity', 1);
8939              }
8940              break;
8941              
8942            case 'gallery':
8943              G.VOM.gallery.PanGallery( ev.deltaX );
8944              break;
8945          }
8946          
8947        });
8948
8949        // PAN END
8950        G.VOM.hammertime.on('panend', function(ev) {
8951          if( !ViewerEvents() ) { return; }
8952
8953          switch( G.VOM.panMode ) {
8954            case 'zoom':
8955              // PAN END in image zoom mode
8956              G.VOM.timeImgChanged = new Date().getTime();
8957              ViewerImagePanSetPosition( G.VOM.panPosX+ev.deltaX, G.VOM.panPosY+ev.deltaY, G.VOM.content.current.$media, true);
8958              break;
8959            case 'media':
8960              var panY = false;
8961              if( !G.VOM.panXOnly ) {
8962                if( Math.abs(ev.deltaY) > 50 && Math.abs(ev.deltaX) < 50 ) {
8963                  // close viewer
8964                  LightboxClose();
8965                  panY = true;
8966                }
8967              }
8968              if( !panY ) {
8969                if( Math.abs( ev.deltaX ) < 50 ) {
8970                  ViewerMediaPanX(0);
8971                }
8972                else {
8973                  ev.deltaX > 50 ? DisplayPreviousMedia( Math.abs(ev.velocityX) ) : DisplayNextMedia( Math.abs(ev.velocityX) );
8974                }
8975              }
8976              G.VOM.panXOnly = false;
8977              break;
8978            case 'gallery':
8979              // PAN END on thumbnail gallery
8980              G.VOM.gallery.posX += ev.deltaX;
8981              G.VOM.gallery.PanGallery( 0 );
8982              G.VOM.gallery.PanGalleryEnd( ev.velocityX );
8983              break;
8984          }
8985
8986          G.VOM.panMode = 'off';
8987        });
8988        
8989				
8990				// ZOOM FEATURE ENABLED
8991        if( G.O.viewerZoom ) {
8992
8993					G.VOM.hammertime.add( new NGHammer.Tap({ event: 'doubletap', taps: 2, interval: 250 }) );
8994          G.VOM.hammertime.add( new NGHammer.Tap({ event: 'singletap' }) );
8995          G.VOM.hammertime.get('doubletap').recognizeWith('singletap');
8996          G.VOM.hammertime.get('singletap').requireFailure('doubletap');
8997
8998          // single tap -> next/previous media
8999          G.VOM.hammertime.on('singletap', function(ev) {
9000
9001						if( !ViewerEvents() ) { return; }
9002						
9003						// Gallery on viewer -> click/touch on one thumbnail -> display corresponding image
9004						if( ev.target.dataset.ngy2_lightbox_thumbnail != undefined ){
9005
9006							var idx = parseInt(ev.target.dataset.ngy2_idx);
9007							var vidx = parseInt(ev.target.dataset.ngy2_vidx);
9008
9009							if( !isNaN(idx) && vidx != G.VOM.content.current.vIdx ) {
9010                
9011                if( vidx > G.VOM.content.current.vIdx ) {
9012                  TriggerCustomEvent('lightboxNextImage');
9013        
9014                  // replace the next media with selected media
9015                  G.VOM.content.next.$media.empty();
9016                  var nextItem = G.I[idx];
9017                  G.VOM.content.next.vIdx = vidx;
9018                  let spreloader = '<div class="nGY2ViewerMediaLoaderDisplayed"></div>';
9019                  if( nextItem.mediaKind == 'img' && nextItem.imageWidth != 0 && nextItem.imageHeight != 0 ) {
9020                    spreloader = '<div class="nGY2ViewerMediaLoaderHidden"></div>';
9021                  }
9022                  G.VOM.content.next.$media.append( spreloader + nextItem.mediaMarkup );
9023                  if( nextItem.mediaKind == 'img' ) {
9024                    G.VOM.ImageLoader.loadImage(VieweImgSizeRetrieved, nextItem);
9025                  }
9026                  else {
9027                    ViewerMediaCenterNotImg( G.VOM.content.next.$media );
9028                  }
9029                  LightboxDisplay('nextImage');
9030
9031                }
9032                else {
9033                  TriggerCustomEvent('lightboxPreviousImage');
9034        
9035                  // replace the previous media with selected media
9036                  G.VOM.content.previous.$media.empty();
9037                  var previousItem = G.I[idx];
9038                  G.VOM.content.previous.vIdx = vidx;
9039                  let spreloader = '<div class="nGY2ViewerMediaLoaderDisplayed"></div>';
9040                  if( previousItem.mediaKind == 'img' && previousItem.imageWidth != 0 && previousItem.imageHeight != 0 ) {
9041                    spreloader = '<div class="nGY2ViewerMediaLoaderHidden"></div>';
9042                  }
9043                  G.VOM.content.previous.$media.append( spreloader + previousItem.mediaMarkup );
9044                  if( previousItem.mediaKind == 'img' ) {
9045                    G.VOM.ImageLoader.loadImage(VieweImgSizeRetrieved, previousItem);
9046                  }
9047                  else {
9048                    ViewerMediaCenterNotImg( G.VOM.content.previous.$media );
9049                  }
9050                  LightboxDisplay('previousImage');
9051                }
9052								return;
9053							}
9054						}
9055						
9056						
9057            StopPropagationPreventDefault(ev.srcEvent);
9058            if( G.VOM.toolbarsDisplayed == false ) {
9059              debounce( ViewerToolsUnHide, 100, false)();
9060							G.VOM.singletapTime = new Date().getTime();
9061            }
9062            else {
9063							// toolbars are displayed -> display next/previous media
9064							if( (new Date().getTime()) - G.VOM.singletapTime < 400 ) { return; }		// to avoid conflict with MOUSEMOVE event
9065
9066              if( G.VOM.content.current.NGY2Item().mediaKind == 'img' && ev.target.className.indexOf('nGY2ViewerMedia') !== -1 ) {
9067                var x =0;
9068								if( ev.srcEvent instanceof MouseEvent ) {
9069									x = ev.srcEvent.pageX;
9070								}
9071								else {
9072									x = ev.srcEvent.changedTouches[0].pageX;
9073								}
9074								if( x < (G.GOM.cache.viewport.w/2) ) {
9075                  DisplayPreviousMedia();
9076                }
9077                else {
9078                  DisplayNextMedia();
9079                }
9080              }
9081            }
9082          });
9083          
9084          // double tap -> zoom
9085          G.VOM.hammertime.on('doubletap', function(ev) {
9086            if( !ViewerEvents() ) { return; }
9087            StopPropagationPreventDefault(ev.srcEvent);
9088            
9089            if( ev.target.className.indexOf('nGY2ViewerMedia') !== -1 ) {
9090              // double tap only on image
9091              if( G.VOM.zoom.isZooming ) {
9092                G.VOM.zoom.isZooming = false;
9093                // G.VOM.zoom.userFactor = 1;
9094                ResizeLightbox(true);
9095              }
9096              else {
9097                if( ViewerZoomStart() ) {
9098                  G.VOM.zoom.userFactor = 1.5;
9099                  ViewerMediaSetPosAndZoom();
9100                }
9101              }
9102            }
9103          });
9104        
9105          // pinch end
9106          G.VOM.hammertime.on('pinchend', function(ev) {
9107            ev.srcEvent.stopPropagation();
9108            ev.srcEvent.preventDefault();  // cancel  mouseenter event
9109            G.VOM.timeImgChanged = new Date().getTime();
9110          });
9111          G.VOM.hammertime.on('pinch', function(ev) {
9112            ev.srcEvent.stopPropagation();
9113            ev.srcEvent.preventDefault();  // cancel  mouseenter event
9114            
9115            if( ViewerZoomStart() ) {
9116              G.VOM.zoom.userFactor = ev.scale;
9117              ViewerZoomMax();
9118              ViewerZoomMin();
9119              ViewerMediaSetPosAndZoom();   // center media
9120            }
9121          });
9122        }
9123				
9124				
9125        else {
9126					// ZOOM FEATURE DISABLED
9127
9128					G.VOM.hammertime.add( new NGHammer.Tap({ event: 'singletap' }) );
9129
9130          // click/tap on image to go to next/previous one
9131          // G.VOM.hammertime.on('tap', function(ev) {
9132          G.VOM.hammertime.on('singletap', function(ev) {
9133            if( !ViewerEvents() ) { return; }
9134            StopPropagationPreventDefault( ev.srcEvent );
9135            if( G.VOM.toolbarsDisplayed == false  ){
9136              // display tools on tap if hidden
9137              debounce( ViewerToolsUnHide, 100, false)();
9138							G.VOM.singletapTime = new Date().getTime();
9139            }
9140            else {
9141							// toolbars are displayed -> display next/previous media
9142							if( (new Date().getTime()) - G.VOM.singletapTime < 400 ) { return; }		// to avoid conflict with MOUSEMOVE event
9143              if( ev.target.className.indexOf('nGY2ViewerMedia') !== -1 ) {
9144								var x = 0;
9145								if( ev.srcEvent instanceof MouseEvent ) {
9146									x = ev.srcEvent.pageX;
9147								}
9148								else {
9149									x = ev.srcEvent.changedTouches[0].pageX;
9150								}
9151                if( x < (G.GOM.cache.viewport.w/2) ) {
9152                  DisplayPreviousMedia();
9153                }
9154                else {
9155                  DisplayNextMedia();
9156                }
9157              }
9158            }
9159            
9160          });
9161        }
9162      }
9163    }
9164
9165
9166    function StopPropagationPreventDefault(e) {
9167      e.stopPropagation();
9168      e.preventDefault();
9169    }
9170
9171    // Hide toolbars on user inactivity
9172    function ViewerToolsHide() {
9173      if( G.VOM.viewerDisplayed ) {
9174        G.VOM.toolbarsDisplayed = false;
9175        ViewerToolsOpacity(0);
9176      }
9177    }
9178    
9179    function ViewerToolsUnHide() {
9180			if( G.VOM.viewerDisplayed ) {
9181        G.VOM.toolbarsDisplayed = true;
9182        ViewerToolsOpacity(1);
9183        G.VOM.toolsHide();        // re-init delay before hide tools+gallery 
9184      }
9185    }
9186    
9187    function ViewerToolsOpacity( op ) {
9188      if( G.VOM.$toolbar != null ) {
9189        G.VOM.$toolbar.css('opacity', op);
9190      }
9191      if( G.VOM.$toolbarTL != null ) {
9192        G.VOM.$toolbarTL.css('opacity', op);
9193      }
9194      if( G.VOM.$toolbarTR != null ) { 
9195        G.VOM.$toolbarTR.css('opacity', op);
9196      }
9197      
9198      // next/previous
9199      G.VOM.$content.find('.nGY2ViewerAreaNext').css('opacity', op);
9200      G.VOM.$content.find('.nGY2ViewerAreaPrevious').css('opacity', op);
9201      
9202      // gallery
9203      // G.VOM.gallery.$elt.css('opacity', op);
9204    }
9205    
9206    
9207    
9208    function ViewerToolsOn() {
9209      // removes all events
9210      G.VOM.$viewer.off('touchstart click', '.ngy2viewerToolAction', ViewerToolsAction); 
9211      
9212      // action button
9213      G.VOM.$viewer.on('touchstart click', '.ngy2viewerToolAction', ViewerToolsAction); 
9214    }
9215
9216    
9217    // Actions of the button/elements
9218    function ViewerToolsAction(e) {
9219      // delay to avoid twice handling on smartphone/tablet (both touchstart click events are fired)
9220      if( (new Date().getTime()) - G.timeLastTouchStart < 300 ) { return; }
9221      G.timeLastTouchStart = new Date().getTime();
9222      
9223      var $this = $(this);
9224      var ngy2action = $this.data('ngy2action');
9225      if( ngy2action == undefined ) { return; }
9226      switch( ngy2action ) {
9227        case 'next':
9228          StopPropagationPreventDefault(e);
9229          DisplayNextMedia();
9230          break;
9231        case 'previous':
9232          StopPropagationPreventDefault(e);
9233          DisplayPreviousMedia();
9234          break;
9235        case 'playPause':
9236          e.stopPropagation();
9237          SlideshowToggle();
9238          break;
9239        case 'zoomIn':
9240          StopPropagationPreventDefault(e);
9241          if( ViewerZoomStart() ) { ViewerZoomIn( true ); }
9242          break;
9243        case 'zoomOut':
9244          StopPropagationPreventDefault(e);
9245          if( ViewerZoomStart() ) { ViewerZoomIn( false ); }
9246          break;
9247        case 'minimize':
9248          // toggle toolbar visibility
9249          StopPropagationPreventDefault(e);
9250          if( G.VOM.toolbarMode == 'std' ) {
9251            ViewerToolbarForVisibilityMin();
9252          }
9253          else {
9254            ViewerToolbarForVisibilityStd();
9255          }
9256          break;
9257        case 'fullScreen':
9258          // Toggle viewer fullscreen mode on/off
9259          e.stopPropagation();
9260          if( ngscreenfull.enabled ) {
9261            ngscreenfull.toggle();
9262          }
9263          break;
9264        case 'info':
9265          e.stopPropagation();
9266          ItemDisplayInfo( G.VOM.content.current.NGY2Item() );
9267          break;
9268        case 'close':
9269          StopPropagationPreventDefault(e);
9270          if( (new Date().getTime()) - G.VOM.timeImgChanged < 400 ) { return; }
9271          LightboxClose();
9272          break;
9273        case 'download':
9274          StopPropagationPreventDefault(e);
9275          DownloadImage(G.VOM.items[G.VOM.content.current.vIdx].ngy2ItemIdx);
9276          break;
9277        case 'share':
9278          StopPropagationPreventDefault(e);
9279          PopupShare(G.VOM.items[G.VOM.content.current.vIdx].ngy2ItemIdx);
9280          break;
9281        case 'linkOriginal':
9282          StopPropagationPreventDefault(e);
9283          OpenOriginal( G.VOM.content.current.NGY2Item() );
9284          break;
9285        case 'rotateLeft':
9286          StopPropagationPreventDefault(e);
9287          ViewerImageRotate(-90);
9288          break;
9289        case 'rotateRight':
9290          StopPropagationPreventDefault(e);
9291          ViewerImageRotate(90);
9292          break;
9293        case 'shoppingcart':
9294          StopPropagationPreventDefault(e);
9295          AddToCart( G.VOM.items[G.VOM.content.current.vIdx].ngy2ItemIdx, 'lightbox');
9296          break;
9297      }
9298      
9299      // custom button
9300      var fu = G.O.fnImgToolbarCustClick;
9301      if( ngy2action.indexOf('custom') == 0  && fu !== null ) {
9302        typeof fu == 'function' ? fu(ngy2action, $this, G.VOM.content.current.NGY2Item() ) : window[fu](ngy2action, $this, G.VOM.content.current.NGY2Item() );
9303      }
9304    }
9305
9306    // rotate displayed image
9307    function ViewerImageRotate( angle ) {
9308      var item = G.VOM.content.current.NGY2Item();
9309      if( item.mediaKind == 'img' ) {
9310        item.rotationAngle += angle;
9311        item.rotationAngle = item.rotationAngle % 360;
9312        if( item.rotationAngle < 0 ) {
9313          item.rotationAngle += 360;
9314        }
9315        ViewerMediaPanX( 0 );
9316        ViewerMediaSetPosAndZoomOne( G.VOM.content.current, true );
9317      }
9318    }
9319     
9320
9321    // Display photo infos in popup/modal
9322    function ItemDisplayInfo( ng2item ) {
9323
9324      var content = '<div class="nGY2PopupOneItem">' + ng2item.title + '</div>';
9325      content    += '<div class="nGY2PopupOneItemText">' + ng2item.description + '</div>';
9326      if( ng2item.author != '' ) {
9327        content  += '<div class="nGY2PopupOneItemText">' + G.O.icons.user + ' ' + ng2item.author + '</div>';
9328      }
9329      if( ng2item.exif.model != '' ) {
9330        content  += '<div class="nGY2PopupOneItemText">' + G.O.icons.config + ' ' + ng2item.exif.model + '</div>';
9331      }
9332      var sexif = G.O.icons.picture + ':';
9333      if( ng2item.exif.flash != '' || ng2item.exif.focallength != '' || ng2item.exif.fstop != '' || ng2item.exif.exposure != '' || ng2item.exif.iso != '' || ng2item.exif.time != '' ) {
9334      sexif += '<br>';
9335      sexif += ng2item.exif.flash == '' ? '' : ' &nbsp; ' + ng2item.exif.flash;
9336      sexif += ng2item.exif.focallength == '' ? '' : ' &nbsp; ' + ng2item.exif.focallength+'mm';
9337      sexif += ng2item.exif.fstop == '' ? '' : ' &nbsp; f' + ng2item.exif.fstop;
9338      sexif += ng2item.exif.exposure == '' ? '' : ' &nbsp; ' + ng2item.exif.exposure+'s';
9339      sexif += ng2item.exif.iso == '' ? '' : ' &nbsp; ' + ng2item.exif.iso+' ISO';
9340      if( ng2item.exif.time != '' ) {
9341        // var date = new Date(parseInt(ng2item.exif.time));
9342        // sexif += ' &nbsp; '+date.toLocaleDateString();
9343        sexif += ' &nbsp; ' + ng2item.exif.time;
9344      }
9345      }
9346      else {
9347        sexif += ' n/a';
9348      }
9349      content += '<div class="nGY2PopupOneItemText">' + sexif + '</div>';
9350
9351      if( ng2item.exif.location != '' ) {
9352        content += '<div class="nGY2PopupOneItemText">'+G.O.icons.location+' <a href="http://maps.google.com/maps?z=12&t=m&q='+encodeURIComponent(ng2item.exif.location)+'" target="_blank">'+ng2item.exif.location+'</a></div>';
9353        // embed google map in iframe (no api key required)
9354        content += '<iframe width="300" height="150" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="https://maps.google.com/maps?&amp;t=m&amp;q='+encodeURIComponent( ng2item.exif.location ) +'&amp;output=embed"></iframe>';  
9355      }
9356      else {
9357        content += '<div class="nGY2PopupOneItemText">' + G.O.icons.location + ': n/a</div>';
9358      }
9359
9360      var r = { title: G.O.icons.viewerInfo, content: content };
9361
9362      // callback
9363      var fu = G.O.fnPopupMediaInfo;
9364      if( fu !== null ) {
9365        typeof fu == 'function' ? r=fu(ng2item, r.title, r.content) : r=window[fu](ng2item, r.title, r.content);
9366      }
9367
9368
9369      Popup( r.title, r.content, 'Left');
9370   
9371    }
9372    
9373
9374    
9375    function ToolbarAddElt( elt ) {
9376      var r = '<div class="ngbt ngy2viewerToolAction ',
9377      e=elt.replace(/^\s+|\s+$/g, '');    // remove trailing/leading whitespace
9378      switch( e ) {
9379        case 'minimizeButton':
9380        case 'minimize':
9381          var ic = G.O.icons.viewerToolbarMin;
9382          if( G.VOM.toolbarMode == 'min' ) {
9383            ic = G.O.icons.viewerToolbarStd;
9384          }
9385          r += 'minimizeButton nGEvent" data-ngy2action="minimize">'+ic+'</div>';
9386          break;
9387        case 'previousButton':
9388        case 'previous':
9389          r += 'previousButton nGEvent" data-ngy2action="previous">'+ G.O.icons.viewerPrevious +'</div>';
9390          break;
9391        case 'pageCounter':
9392          r += 'pageCounter nGEvent"></div>';
9393          break;
9394        case 'nextButton':
9395        case 'next':
9396          r += 'nextButton nGEvent" data-ngy2action="next">'+ G.O.icons.viewerNext +'</div>';
9397          break;
9398        case 'playPauseButton':
9399        case 'playPause':
9400          r += 'playButton playPauseButton nGEvent" data-ngy2action="playPause">'+ G.O.icons.viewerPlay +'</div>';
9401          break;
9402        case 'rotateLeft':
9403          r += 'rotateLeftButton nGEvent" data-ngy2action="rotateLeft">'+ G.O.icons.viewerRotateLeft +'</div>';
9404          break;
9405        case 'rotateRight':
9406          r += 'rotateRightButton nGEvent" data-ngy2action="rotateRight">'+ G.O.icons.viewerRotateRight +'</div>';
9407          break;
9408        case 'downloadButton':
9409        case 'download':
9410          r += 'downloadButton nGEvent" data-ngy2action="download">'+ G.O.icons.viewerDownload +'</div>';
9411          break;
9412        case 'zoomButton':
9413        case 'zoom':
9414          r += 'nGEvent" data-ngy2action="zoomIn">'+ G.O.icons.viewerZoomIn +'</div><div class="ngbt ngy2viewerToolAction nGEvent" data-ngy2action="zoomOut">'+ G.O.icons.viewerZoomOut +'</div>';
9415          break;
9416        case 'fullscreenButton':
9417        case 'fullscreen':
9418          var s = G.O.icons.viewerFullscreenOn;
9419          if( ngscreenfull.enabled && G.VOM.viewerIsFullscreen ) {
9420            s = G.O.icons.viewerFullscreenOff;
9421          }
9422          r += 'setFullscreenButton fullscreenButton nGEvent" data-ngy2action="fullScreen">'+s+'</div>';
9423          break;
9424        case 'infoButton':
9425        case 'info':
9426          r += 'infoButton nGEvent" data-ngy2action="info">'+ G.O.icons.viewerInfo +'</div>';
9427          break;
9428        case 'linkOriginalButton':
9429        case 'linkOriginal':
9430          r += 'linkOriginalButton nGEvent" data-ngy2action="linkOriginal">' + G.O.icons.viewerLinkOriginal + '</div>';
9431          break;
9432        case 'closeButton':
9433        case 'close':
9434          r += 'closeButton nGEvent" data-ngy2action="close">'+ G.O.icons.buttonClose +'</div>';
9435          break;
9436        case 'shareButton':
9437        case 'share':
9438          r += 'nGEvent" data-ngy2action="share">'+ G.O.icons.viewerShare +'</div>';
9439          break;
9440        case 'label':
9441          r += '"><div class="label"><div class="title nGEvent" itemprop="name"></div><div class="description nGEvent" itemprop="description"></div></div></div>';
9442          break;
9443        case 'shoppingcart':
9444          r += 'closeButton nGEvent" data-ngy2action="shoppingcart">'+ G.O.icons.viewerShoppingcart +'</div>';
9445          break;
9446        default:
9447          // custom button
9448          if( e.indexOf('custom') == 0 ) {
9449            var t = '';
9450            // content to display from custom script
9451            var fu = G.O.fnImgToolbarCustInit;
9452            if( fu !== null ) {
9453              typeof fu == 'function' ? fu(e) : window[fu](e);
9454            }
9455            if( t == undefined || t == '' ) {
9456              // content from icons
9457              var n = e.substring(6);
9458              t = G.O.icons['viewerCustomTool'+n];
9459            }
9460            r += 'ngy2CustomBtn ' + e + ' nGEvent" data-ngy2action="' + e + '">' + t + '</div>';
9461          }
9462          else {
9463            r = '';
9464          }
9465          break;
9466      }
9467      return r;
9468    }
9469    
9470    
9471    // toggle slideshow mode on/off
9472    function SlideshowToggle(){
9473      if( G.VOM.playSlideshow ) {
9474        window.clearTimeout(G.VOM.playSlideshowTimerID);
9475        G.VOM.playSlideshow = false;
9476        G.VOM.$viewer.find('.playPauseButton').html(G.O.icons.viewerPlay);
9477      }
9478      else {
9479        G.VOM.playSlideshow = true;
9480        DisplayNextMedia();
9481        G.VOM.$viewer.find('.playPauseButton').html(G.O.icons.viewerPause);
9482      }
9483    }
9484
9485    function ViewerToolbarForVisibilityStd() {
9486      G.VOM.toolbarMode = 'std';
9487      
9488      var sTB = '';
9489      var t = G.O.viewerToolbar.standard.split(',');
9490      for( var i = 0, lt = t.length; i < lt; i++) {
9491        sTB += ToolbarAddElt( t[i] );
9492      }
9493      G.VOM.$toolbar.find('.toolbar').html(sTB);
9494      ViewerToolbarElementContent();
9495    }
9496    
9497    function ViewerToolbarForVisibilityMin() {
9498      if( G.O.viewerToolbar.minimized == undefined || G.O.viewerToolbar.minimized == '' ) {
9499        ViewerToolbarForVisibilityStd();
9500      }
9501      else {
9502        G.VOM.toolbarMode = 'min';
9503        var sTB = '';
9504        var t = G.O.viewerToolbar.minimized.split(',');
9505        for( var i = 0, lt = t.length; i < lt; i++) {
9506          sTB += ToolbarAddElt( t[i] );
9507        }
9508        G.VOM.$toolbar.find('.toolbar').html(sTB);
9509        ViewerToolbarElementContent();
9510      }
9511    }
9512    
9513    function ViewerToolbarElementContent() {
9514    
9515      var vomIdx = G.VOM.content.current.vIdx;
9516      if( vomIdx == null ) { return; }
9517      
9518      var item = G.VOM.content.current.NGY2Item();
9519    
9520      // LABEL
9521      var setTxt = false;
9522      // set title
9523      if( item.title !== undefined && item.title != '' ) {
9524        G.VOM.$viewer.find('.ngy2viewerToolAction').find('.title').html(item.title);
9525        setTxt = true;
9526      }
9527      else {
9528        G.VOM.$viewer.find('.ngy2viewerToolAction').find('.title').html('');
9529      }
9530      // set description
9531      if( item.description !== undefined && item.description != '' ) {
9532        G.VOM.$viewer.find('.ngy2viewerToolAction').find('.description').html(item.description);
9533        setTxt = true;
9534      }
9535      else {
9536        G.VOM.$viewer.find('.ngy2viewerToolAction').find('.description').html('');
9537      }
9538      
9539      if( setTxt ) {
9540        G.VOM.$viewer.find('.ngy2viewerToolAction').find('.label').show();
9541      }
9542      else {
9543        G.VOM.$viewer.find('.ngy2viewerToolAction').find('.label').hide();
9544      }
9545      
9546      // set page number
9547      var viewerMaxImages = G.VOM.items.length;
9548      if( viewerMaxImages > 0 ) {
9549        G.VOM.$viewer.find('.pageCounter').html((G.VOM.items[vomIdx].mediaNumber)+'/'+viewerMaxImages);
9550      }
9551      
9552      // custom elements
9553      var $cu = G.VOM.$viewer.find('.ngy2CustomBtn');
9554      var fu = G.O.fnImgToolbarCustDisplay;
9555      if( $cu.length > 0 && fu !== null ) {
9556        typeof fu == 'function' ? fu($cu, item) : window[fu]($cu, item);
9557      }
9558      
9559      // set event handlers again
9560      ViewerToolsOn();
9561    }
9562    
9563    // Pan the media container in the lightbox (left/right)
9564    function ViewerMediaPanX( posX ) {
9565      G.VOM.swipePosX = posX;
9566      if( G.CSStransformName == null ) {
9567        // no pan if CSS transform not supported
9568        // G.VOM.$mediaCurrent.css({ left: posX }); 
9569      }
9570      else {
9571      
9572        // pan left/right the current media
9573        // window.ng_draf( function() {
9574          G.VOM.content.current.$media[0].style[G.CSStransformName] = 'translate(' + posX + 'px, 0px)';
9575        // });
9576
9577        var itemPrevious = G.VOM.content.previous.NGY2Item();
9578        var itemNext = G.VOM.content.next.NGY2Item();
9579        
9580        // next/previous media
9581        if(  G.O.imageTransition.startsWith('SWIPE') ) {
9582          if( itemPrevious.mediaTransition() ) {
9583            ViewerSetMediaVisibility(G.VOM.content.previous, 1);
9584          }
9585          if( itemNext.mediaTransition() ) {
9586            ViewerSetMediaVisibility(G.VOM.content.next, 1);
9587          }
9588
9589          var sc = Math.min( Math.max( Math.abs(posX) / G.VOM.window.lastWidth, .8), 1);
9590          if( G.O.imageTransition == 'SWIPE' ) { sc = 1; }
9591
9592          if( posX > 0 ) {
9593            let dir = G.VOM.window.lastWidth;
9594            if( itemPrevious.mediaTransition() ) {
9595              // window.ng_draf( function() {
9596                G.VOM.content.previous.$media[0].style[G.CSStransformName] = 'translate(' + (-dir + posX) + 'px, 0px) scale(' + sc + ')';
9597              // });
9598            }
9599            if( itemNext.mediaTransition() ) {
9600              // window.ng_draf( function() {
9601                G.VOM.content.next.$media[0].style[G.CSStransformName] = 'translate(' + (dir) + 'px, 0px) scale(' + sc + ')';
9602              // });
9603            }
9604          }
9605          else {
9606            let dir = -G.VOM.window.lastWidth;
9607            if( itemNext.mediaTransition() ) {
9608              // window.ng_draf( function() {
9609                G.VOM.content.next.$media[0].style[G.CSStransformName] = 'translate(' + (-dir + posX) + 'px, 0px) scale(' + sc + ')';
9610              // });
9611            }
9612            if( itemPrevious.mediaTransition() ) {
9613              // window.ng_draf( function() {
9614                G.VOM.content.previous.$media[0].style[G.CSStransformName] = 'translate(' + (dir) + 'px, 0px) scale(' + sc + ')';
9615              // });
9616            }
9617          }
9618        }
9619        
9620        
9621        if(  G.O.imageTransition == 'SLIDEAPPEAR' ) {
9622          G.VOM.content.previous.$media[0].style[G.CSStransformName] = '';
9623          G.VOM.content.next.$media[0].style[G.CSStransformName] = '';
9624          if( posX < 0 ) {
9625            let o = (-posX) / G.VOM.window.lastWidth;
9626            if( itemNext.mediaTransition() ) {
9627              ViewerSetMediaVisibility(G.VOM.content.next, o);
9628            }
9629            if( itemPrevious.mediaTransition() ) {
9630              ViewerSetMediaVisibility(G.VOM.content.previous, 0);
9631            }
9632          }
9633          else {
9634            let o = posX / G.VOM.window.lastWidth;
9635            if( itemPrevious.mediaTransition() ) {
9636              ViewerSetMediaVisibility(G.VOM.content.previous, o);
9637            }
9638            if( itemNext.mediaTransition() ) {
9639              ViewerSetMediaVisibility(G.VOM.content.next, 0);
9640            }
9641          }
9642        }
9643      }
9644    }
9645    
9646    // Display next image
9647    function DisplayNextMedia( velocity ) {
9648      velocity = velocity || 0;
9649      
9650      if( G.VOM.viewerMediaIsChanged || ((new Date().getTime()) - G.VOM.timeImgChanged < 300) ) { return; }
9651      
9652      TriggerCustomEvent('lightboxNextImage');
9653      LightboxDisplay('nextImage', velocity);
9654    };
9655    
9656    // Display previous image
9657    function DisplayPreviousMedia( velocity ) {
9658      velocity = velocity || 0;
9659
9660      if( G.VOM.viewerMediaIsChanged || ((new Date().getTime()) - G.VOM.timeImgChanged < 300) ) { return; }
9661      if( G.VOM.playSlideshow ) {
9662        SlideshowToggle();
9663      }
9664      
9665      TriggerCustomEvent('lightboxPreviousImage');
9666      LightboxDisplay( 'previousImage', velocity);
9667    };
9668
9669
9670
9671    // Display image (with animation if possible)
9672    function LightboxDisplay( displayType, velocity ) {
9673
9674      velocity = velocity || 0;
9675
9676      if( G.O.debugMode && console.timeline ) { console.timeline('nanogallery2_viewer'); }
9677
9678      if( G.VOM.playSlideshow ) { window.clearTimeout( G.VOM.playSlideshowTimerID ); }
9679      
9680      var current_content_item = null;
9681      var new_content_item  = null;
9682
9683      G.VOM.timeImgChanged = new Date().getTime();
9684      G.VOM.viewerMediaIsChanged = true;
9685      G.VOM.zoom.isZooming = false;
9686      ResizeLightbox(true);
9687
9688      switch( displayType ) {
9689        case '':
9690            current_content_item = G.VOM.content.current;
9691            new_content_item = G.VOM.content.current;
9692          break;
9693        case 'previousImage':
9694            current_content_item = G.VOM.content.current;
9695            new_content_item = G.VOM.content.previous;
9696          break;
9697        default:
9698            current_content_item = G.VOM.content.current;
9699            new_content_item = G.VOM.content.next;
9700      }
9701
9702      // SetLocationHash( next_ng2item.albumID, next_ng2item.GetID() );
9703      SetLocationHash( new_content_item.NGY2Item().albumID, new_content_item.NGY2Item().GetID() );
9704
9705      if( displayType == '' ) {
9706        // first media -> no transition -> exit
9707        return;
9708      }
9709      
9710      // animation duration is proportional of the remaining distance
9711      var vP = G.GOM.cache.viewport;
9712      var t_easing = '';
9713      var t_dur = 500 * (vP.w - Math.abs(G.VOM.swipePosX)) / vP.w;
9714      if( velocity > 0 ) {
9715        // velocity = pixels/millisecond
9716         t_dur = Math.min( (vP.w - Math.abs(G.VOM.swipePosX)) / velocity, t_dur);
9717         t_easing = 'linear';     // use linear to avoid a slow-down in the transition after user swipe
9718      }
9719      
9720        
9721      // animate the image transition between 2 medias
9722        
9723      if( G.CSStransformName == null  ) {
9724        // no CSS transform support -> no animation
9725        ViewerSetMediaVisibility(new_content_item, 1);
9726        ViewerSetMediaVisibility(current_content_item, 1);
9727        LightboxDisplayFinalize(displayType);
9728      }
9729      else {
9730        switch( G.O.imageTransition ) {
9731          case 'SWIPE':
9732          case 'SWIPE2':
9733            var dir = ( displayType == 'nextImage' ? - vP.w : vP.w );
9734            new_content_item.$media[0].style[G.CSStransformName] = 'translate(' + (-dir) + 'px, 0px) '
9735
9736            if( velocity == 0 ) {
9737              t_easing = G.O.imageTransition == 'swipe' ? 'easeInOutSine' : 'easeOutCubic';
9738            }
9739            
9740            ViewerSetMediaVisibility(G.VOM.content.current, 1);
9741            G.VOM.content.current.$media[0].style[G.CSStransformName] = 'translate(0px, 0px)';
9742            ViewerSetMediaVisibility(new_content_item, 1);
9743
9744            new NGTweenable().tween({
9745              from:         { t: G.VOM.swipePosX  },
9746              to:           { t: (displayType == 'nextImage' ? - vP.w : vP.w) },
9747              attachment:   { dT: displayType, new_content_item: new_content_item, dir: dir, media_transition: new_content_item.NGY2Item().mediaTransition()},
9748              // delay:        30,
9749              duration:     t_dur,
9750              easing:       ( t_easing ),
9751              step:         function (state, att) {
9752                // current displayed media
9753                G.VOM.content.current.$media[0].style[G.CSStransformName] = 'translate(' + state.t + 'px, 0px)';
9754
9755                // new media
9756                if( att.media_transition ) {
9757                  // new media supports transition
9758                  var sc = Math.min( Math.max( (Math.abs(state.t)) / G.VOM.window.lastWidth, .8), 1);
9759                  if( G.O.imageTransition == 'SWIPE' ) { sc = 1; }
9760                  att.new_content_item.$media[0].style[G.CSStransformName] = 'translate(' + (-att.dir+state.t) + 'px, 0px) scale(' + sc + ')';
9761                }
9762              },
9763              finish:       function (state, att) {
9764                G.VOM.content.current.$media[0].style[G.CSStransformName] = '';
9765                ViewerSetMediaVisibility(G.VOM.content.current, 0);
9766                att.new_content_item.$media[0].style[G.CSStransformName] = '';
9767                LightboxDisplayFinalize(att.dT);
9768              }
9769            });
9770            break;
9771            
9772          case 'SLIDEAPPEAR':
9773          default:
9774            // var dir=(displayType == 'nextImage' ? - vP.w : vP.w);
9775            var op = (Math.abs(G.VOM.swipePosX)) / G.VOM.window.lastWidth;
9776            new_content_item.$media[0].style[G.CSStransformName] = '';
9777            if( velocity == 0 ) {
9778              t_easing ='easeInOutSine';
9779            }
9780            new NGTweenable().tween({
9781              from:         { o: op, t: G.VOM.swipePosX },
9782              to:           { o: 1,  t: (displayType == 'nextImage' ? - vP.w : vP.w) },
9783              attachment:   { dT: displayType, new_content_item:new_content_item, media_transition: new_content_item.NGY2Item().mediaTransition()  },
9784              delay:        30,
9785              duration:     t_dur,
9786              easing:       t_easing,
9787              step:         function (state, att) {
9788                // current media - translate
9789                G.VOM.content.current.$media[0].style[G.CSStransformName]= 'translate('+state.t+'px, 0px)';
9790
9791                // new media - opacity
9792                if( att.media_transition ) {
9793                  // new media supports transition
9794                  ViewerSetMediaVisibility(att.new_content_item, state.o);
9795                }
9796              },
9797              finish:       function (state, att) {
9798                G.VOM.content.current.$media[0].style[G.CSStransformName]= '';
9799                LightboxDisplayFinalize(att.dT);
9800              }
9801            });
9802            break;
9803        }
9804      }
9805    
9806    }
9807  
9808
9809    function LightboxDisplayFinalize( displayType ) {
9810
9811      var newVomIdx = 0;
9812      switch( displayType ) {
9813        case '':
9814					// first media to display in lightbox
9815					newVomIdx = G.VOM.content.current.vIdx;
9816          break;
9817        case 'previousImage':
9818					// previous media
9819					newVomIdx = G.VOM.content.previous.vIdx;
9820          break;
9821        default:
9822					// next media
9823					newVomIdx = G.VOM.content.next.vIdx;
9824      }
9825
9826			
9827
9828      G.VOM.content.current.vIdx = newVomIdx;
9829      G.VOM.content.next.vIdx = G.VOM.IdxNext();
9830      G.VOM.content.previous.vIdx = G.VOM.IdxPrevious();
9831			G.VOM.gallery.Resize();
9832			G.VOM.gallery.SetThumbnailActive();
9833      
9834      var ngy2item = G.VOM.content.current.NGY2Item();
9835
9836      ViewerToolbarElementContent();
9837      if( G.O.debugMode && console.timeline ) { console.timelineEnd('nanogallery2_viewer'); }
9838
9839      var fu=G.O.fnImgDisplayed;
9840      if( fu !== null ) {
9841        typeof fu == 'function' ? fu(ngy2item) : window[fu](ngy2item);
9842      }
9843      
9844      G.VOM.swipePosX = 0;
9845      if( displayType != '' ) {
9846        // not on first media display
9847        G.VOM.content.current.$media.removeClass('imgCurrent');
9848
9849        var $tmp = G.VOM.content.current.$media;
9850        switch( displayType ) {
9851          case 'nextImage':
9852            G.VOM.content.current.$media = G.VOM.content.next.$media;
9853            G.VOM.content.next.$media = $tmp;
9854            break;
9855          case 'previousImage':
9856            G.VOM.content.current.$media =  G.VOM.content.previous.$media;
9857             G.VOM.content.previous.$media = $tmp;
9858            break;
9859        }
9860      }
9861      
9862      G.VOM.content.current.$media.addClass('imgCurrent');
9863      
9864      // re-sort the media containers --> current on top
9865      var $pans = G.VOM.$content.find('.nGY2ViewerMediaPan');
9866      G.VOM.content.current.$media.insertAfter($pans.last());
9867      
9868      if( ngy2item.mediaKind == 'img' && ngy2item.imageWidth == 0 ) {
9869        ViewerSetMediaVisibility(G.VOM.content.current, 0);
9870      }
9871      else {
9872        G.VOM.content.current.$media.children().eq(0).attr('class', 'nGY2ViewerMediaLoaderHidden');    // hide preloader
9873        ViewerSetMediaVisibility(G.VOM.content.current, 1);
9874      }
9875
9876      
9877      // set the new NEXT media
9878      G.VOM.content.next.$media.empty();
9879      var nextItem = G.VOM.content.next.NGY2Item();
9880      var spreloader = '<div class="nGY2ViewerMediaLoaderDisplayed"></div>';
9881      if( nextItem.mediaKind == 'img' && nextItem.imageWidth != 0 && nextItem.imageHeight != 0 ) {
9882        spreloader = '<div class="nGY2ViewerMediaLoaderHidden"></div>';
9883      }
9884      G.VOM.content.next.$media.append( spreloader + nextItem.mediaMarkup );
9885      ViewerSetMediaVisibility(G.VOM.content.next, 0);
9886      ViewerSetMediaVisibility(G.VOM.content.previous, 0);
9887      if( nextItem.mediaKind == 'img' ) {
9888        G.VOM.ImageLoader.loadImage(VieweImgSizeRetrieved, nextItem);
9889      }
9890      else {
9891        ViewerMediaCenterNotImg( G.VOM.content.next.$media );
9892      }
9893
9894      // set the new PREVIOUS media
9895      G.VOM.content.previous.$media.empty();
9896      var previousItem = G.VOM.content.previous.NGY2Item();
9897      spreloader = '<div class="nGY2ViewerMediaLoaderDisplayed"></div>';
9898      if( previousItem.mediaKind == 'img' && previousItem.imageWidth != 0 && previousItem.imageHeight != 0 ) {
9899        spreloader = '<div class="nGY2ViewerMediaLoaderHidden"></div>';
9900      }
9901      G.VOM.content.previous.$media.append( spreloader + previousItem.mediaMarkup );
9902      ViewerSetMediaVisibility(G.VOM.content.previous, 0);
9903      ViewerSetMediaVisibility(G.VOM.content.next, 0);
9904      if( previousItem.mediaKind == 'img' ) {
9905        G.VOM.ImageLoader.loadImage( VieweImgSizeRetrieved, previousItem );
9906      }
9907      else {
9908        ViewerMediaCenterNotImg( G.VOM.content.previous.$media );
9909      }
9910
9911        
9912      // slideshow mode - wait until image is loaded to start the delay for next image
9913      if( G.VOM.playSlideshow ) {
9914        G.VOM.content.current.$media.children().eq(1).ngimagesLoaded().always( function( instance ) {
9915          if( G.VOM.playSlideshow ) {
9916            // in the meantime the user could have stopped the slideshow
9917            G.VOM.playSlideshowTimerID = window.setTimeout( function(){ DisplayNextMedia(); }, G.VOM.slideshowDelay );
9918          }
9919        });
9920      }
9921      
9922      // close viewer when user clicks outside of the image
9923      // G.VOM.$mediaCurrent.on("click",function(e){
9924      //  e.stopPropagation();
9925      //  if( (new Date().getTime()) - G.VOM.timeImgChanged < 400 ) { return; }
9926      //  StopPropagationPreventDefault(e);
9927      //  LightboxClose(G.VOM.currItemIdx);
9928      //  return false;
9929      // });
9930
9931      ResizeLightbox();
9932
9933      G.VOM.viewerMediaIsChanged = false;
9934      TriggerCustomEvent('lightboxImageDisplayed');
9935      
9936    }
9937
9938    
9939    // Is fired as soon as the size of an image has been retrieved (the download may not be finished)
9940    function VieweImgSizeRetrieved(w, h, item, n) {
9941
9942      item.imageWidth = w;
9943      item.imageHeight = h;
9944  
9945      // image sized retrieved for currently displayed media
9946      if( G.VOM.content.current.NGY2Item() == item ) {
9947        // it is the current displayed media
9948        G.VOM.content.current.$media.children().eq(0).attr('class', 'nGY2ViewerMediaLoaderHidden');    // hide preloader
9949        ViewerSetMediaVisibility(G.VOM.content.current, 1);
9950        G.VOM.zoom.userFactor = 1;
9951      }
9952      
9953      if( G.VOM.content.next.NGY2Item() == item ) {   // next media
9954        G.VOM.content.next.$media.children().eq(0).attr('class', 'nGY2ViewerMediaLoaderHidden');    // hide preloader
9955      }
9956      if( G.VOM.content.previous.NGY2Item() == item ) {   // previous media
9957        G.VOM.content.previous.$media.children().eq(0).attr('class', 'nGY2ViewerMediaLoaderHidden');    // hide preloader
9958      }
9959      
9960      ViewerMediaSetPosAndZoom();
9961
9962    }
9963
9964    // Viewer - Set the visibility of the media and it's container
9965    // function ViewerSetMediaVisibility(item, $media, opacity ) {
9966    function ViewerSetMediaVisibility( content_item, opacity ) {
9967
9968			var item = content_item.NGY2Item();
9969      var $media = content_item.$media;
9970      
9971      if( item.mediaKind == 'img' && item.imageWidth == 0 ) {
9972        // do not display image if width is unknown (--> callback will set the width when know)
9973        // setting opacity to 0 is not enough -> it is mandatory to change also the visibility to hidden to avoid responds to events (click/touch)
9974        // $media.children().css({ opacity: 0, visibility: 'hidden' });
9975        $media.children().eq(1).css({ opacity: 0, visibility: 'hidden' });    // hide media
9976        // $media.css({ opacity: 0, visibility: 'hidden' });
9977        return;
9978      }
9979      
9980      if( opacity == 0 ) {
9981        // setting opacity to 0 is not enough -> it is mandatory to change also the visibility to hidden to avoid responds to events (click/touch)
9982        // $media.css({ opacity: 0, visibility: 'hidden' });
9983        $media.children().css({ opacity: 0, visibility: 'hidden' });    // hide media
9984      }
9985      else {
9986        // $media.css({ opacity: opacity, visibility: 'visible' });
9987        $media.children().css({ opacity: opacity, visibility: 'visible' });      // display media
9988      }
9989    }
9990    
9991    
9992    // Close the internal lightbox
9993    function LightboxClose( vomIdx ) {
9994
9995
9996      if( vomIdx == undefined ) {
9997        vomIdx = G.VOM.content.current.vIdx;
9998      }
9999
10000      G.VOM.viewerMediaIsChanged = false;
10001
10002      if( G.VOM.viewerDisplayed ) {
10003
10004        // set scrollbar visible again
10005        jQuery("body").removeClass("nGY2_body_scrollbar");
10006        jQuery("#nGY2_body_scrollbar_style").remove();
10007        
10008        if( G.VOM.playSlideshow ) {
10009          window.clearTimeout( G.VOM.playSlideshowTimerID );
10010          G.VOM.playSlideshow = false;
10011        }
10012
10013        G.VOM.hammertime.destroy();
10014        G.VOM.hammertime = null;
10015
10016        if( ngscreenfull.enabled && G.VOM.viewerIsFullscreen ) {
10017          G.VOM.viewerIsFullscreen = false;
10018          ngscreenfull.exit();
10019        }
10020        
10021        // G.VOM.$baseCont.hide(0).off().show(0).html('').remove();
10022        // G.VOM.$baseCont.remove();         // does not work... (?)
10023        jQuery('.nGY2ViewerContainer').remove();
10024        G.VOM.$baseCont = null;
10025        G.VOM.viewerDisplayed = false;
10026
10027        if( G.O.lightboxStandalone ) { return; }
10028
10029        if( G.O.thumbnailAlbumDisplayImage ) {
10030          // content of album displayed directly in lightbox (no gallery display for album content)
10031          if( vomIdx == null ) {
10032            // lightbox closed with browser back-button
10033            // the gallery is already displayed
10034          }
10035          else {
10036            var item = G.I[G.VOM.items[vomIdx].ngy2ItemIdx];
10037            var parent = NGY2Item.Get(G, item.albumID);
10038            if( G.GOM.albumIdx != parent.albumID ) {
10039              // display album only if not already displayed
10040              DisplayAlbum('-1', parent.albumID);
10041            }
10042            else {
10043              GalleryResize();        
10044              SetLocationHash( '', '' );
10045              ThumbnailHoverReInitAll();
10046            }
10047          }
10048            // DisplayAlbum( '-', G.I[G.VOM.items[vomIdx].ngy2ItemIdx].albumID );
10049        }
10050        else {
10051          if( vomIdx != null ) {
10052            if( G.GOM.albumIdx == -1 ) {
10053              // album not displayed --> display gallery
10054              DisplayAlbum( '', G.I[G.VOM.items[vomIdx].ngy2ItemIdx].albumID );
10055            }
10056            else {
10057              GalleryResize();        
10058              SetLocationHash( G.I[G.VOM.items[vomIdx].ngy2ItemIdx].albumID, '' );
10059              ThumbnailHoverReInitAll();
10060            }
10061          }
10062        }
10063        G.VOM.timeImgChanged = new Date().getTime();
10064      }
10065    }
10066
10067    
10068    // Lightbox resized -> reposition elements
10069    function ResizeLightbox( forceUpdate ) {
10070      forceUpdate = typeof forceUpdate !== 'undefined' ? forceUpdate : false;
10071      
10072      if( G.VOM.$toolbar === null ) { return; }   // viewer build not finished
10073      
10074      
10075      // window.requestAnimationFrame( function() {    // synchronize with screen
10076      var windowsW = G.VOM.$viewer.width();
10077      var windowsH = G.VOM.$viewer.height();
10078      var $elt = G.VOM.content.current.$media.children().eq(1);
10079      if( $elt == null || G.VOM.content.current.vIdx == -1 ) { return; }
10080
10081      if( !forceUpdate && G.VOM.window.lastWidth == windowsW  && G.VOM.window.lastHeight == windowsH ) { return; }
10082      
10083      G.VOM.window.lastWidth = windowsW;
10084      G.VOM.window.lastHeight = windowsH;
10085
10086      // var $tb = G.VOM.$toolbar.find('.toolbar');
10087      // var tb_OHt = $tb.outerHeight(true);
10088
10089
10090      var galleryHeight = 0;
10091      var cBottom = 0;
10092			// Height of the thumbnails gallery
10093			if( G.O.viewerGallery != 'none' ) {
10094        galleryHeight = G.O.viewerGalleryTHeight + 10;
10095			}
10096			if( G.O.viewerGallery == 'bottom' ) {
10097       cBottom = galleryHeight;
10098			}
10099
10100			
10101      switch( G.O.viewerToolbar.position ) {
10102        case 'top':
10103        case 'topOverImage':
10104          G.VOM.$content.css({height: windowsH, width: windowsW, top: 0  });
10105          G.VOM.$toolbar.css({top: 0, bottom: ''});
10106          break;
10107        // case 'top':
10108          // windowsH -= tb_OHt;
10109          // G.VOM.$content.css({height: windowsH, width: windowsW, top: tb_OHt  });
10110          // G.VOM.$toolbar.css({top: 0});
10111          // break;
10112        case 'bottom':
10113        case 'bottomOverImage':
10114        default:
10115          windowsH -= cBottom;
10116          G.VOM.$content.css({height: windowsH, width: windowsW, bottom: -cBottom, top: 0  });
10117          G.VOM.$toolbar.css({bottom: galleryHeight});
10118          break;
10119        // case 'bottom':
10120        // default:
10121          // windowsH -= tb_OHt;
10122          // G.VOM.$content.css({ width: windowsW, top: 0, bottom: tb_OHt });
10123          // G.VOM.$toolbar.css({bottom: galleryHeight});
10124          // break;
10125      }
10126			
10127			
10128      if( !G.VOM.viewerMediaIsChanged && G.VOM.zoom.isZooming ) {
10129        ViewerMediaSetPosAndZoom();
10130      }
10131      else {
10132				if( !G.VOM.zoom.isZooming && ( G.VOM.zoom.userFactor != 0 || G.VOM.panPosX != 0 || G.VOM.panPosY != 0 || G.VOM.zoom.posX != 0 || G.VOM.zoom.posY != 0 )) {
10133					// animate image zoom factor and position back to initial values
10134					G.VOM.zoom.isZooming= true;		// activate zooming temporarily
10135          new NGTweenable().tween({
10136            from:           { userFactor: G.VOM.zoom.userFactor, panPosX: G.VOM.panPosX, panPosY: G.VOM.panPosY, zoomPosX: G.VOM.zoom.posX, zoomPosY: G.VOM.zoom.posY },
10137            to:           { userFactor: 1, panPosX: 0, panPosY: 0, zoomPosX: 0, zoomPosY: 0 },
10138            easing:       'easeInOutSine',
10139            delay:        0,
10140            duration:     150,
10141            step:         function (state) {
10142							G.VOM.zoom.userFactor = state.userFactor;
10143							G.VOM.panPosX = state.panPosX;
10144							G.VOM.panPosY = state.panPosY;
10145							G.VOM.zoom.posX = state.zoomPosX;
10146							G.VOM.zoom.posY = state.zoomPosY;
10147							ViewerMediaSetPosAndZoom();
10148            },
10149            finish:       function (state) {
10150							G.VOM.zoom.isZooming=false;
10151            }
10152          });
10153			
10154				}
10155				else {
10156					G.VOM.zoom.userFactor = 1;
10157					G.VOM.zoom.isZooming = false;
10158					G.VOM.panPosX = 0;
10159					G.VOM.panPosY = 0;
10160					G.VOM.zoom.posX = 0;
10161					G.VOM.zoom.posY = 0;
10162					ViewerMediaSetPosAndZoom();
10163				}
10164      }
10165    }
10166
10167		// Retrieve the first parent element which is scrollable
10168		// source: ncubica - https://stackoverflow.com/questions/35939886/find-first-scrollable-parent
10169		// returns null if nothing found
10170		function getScrollableParent (node) {
10171			const regex = /(auto|scroll)/;
10172			const parents = (_node, ps) => {
10173				if (_node.parentNode === null) { return ps; }
10174				return parents(_node.parentNode, ps.concat([_node]));
10175			};
10176
10177			const style = (_node, prop) => getComputedStyle(_node, null).getPropertyValue(prop);
10178			const overflow = _node => style(_node, 'overflow') + style(_node, 'overflow-y') + style(_node, 'overflow-x');
10179			const scroll = _node => regex.test(overflow(_node));
10180
10181			const scrollParent = (_node) => {
10182				if (!(_node instanceof HTMLElement || _node instanceof SVGElement)) {
10183					return undefined;
10184				}
10185
10186				const ps = parents(_node.parentNode, []);
10187
10188				for (let i = 0; i < ps.length; i += 1) {
10189					if( ps[i] === document.body ) {
10190						return null;
10191					}
10192					if (scroll(ps[i])) {
10193						return ps[i];
10194					}
10195				}
10196
10197				return document.scrollingElement || document.documentElement;
10198			};
10199
10200			return scrollParent(node);
10201		};
10202
10203
10204
10205    /** @function BuildSkeleton */
10206    /** Build the gallery structure **/
10207    function BuildSkeleton() {
10208
10209		
10210      // store markup if defined
10211      // var $elements = G.$E.base.children('a');
10212      var $elements = G.$E.base.children();
10213      if( $elements.length > 0 ) {
10214        G.O.$markup = $elements;
10215      }
10216    
10217      if( !G.O.lightboxStandalone ) {
10218        G.$E.base.text('');
10219        G.$E.base.addClass('ngy2_container');
10220      
10221        // RTL or LTR
10222        // var sRTL='';
10223        // if( G.O.RTL ) {
10224          // sRTL = 'style="text-align:right;direction:rtl;"';
10225        // }
10226      
10227        // theme
10228        G.$E.base.addClass(G.O.theme)
10229        // gallery color scheme
10230        SetGalleryTheme();
10231
10232        // Hide icons (thumbnails and breadcrumb)
10233        if( G.O.thumbnailLabel.get('hideIcons') ) {
10234          G.O.icons.thumbnailAlbum = '';
10235          G.O.icons.thumbnailImage = '';
10236        }
10237
10238        // Navigation bar
10239        var styleNavigation="";
10240        if( G.O.navigationFontSize != undefined && G.O.navigationFontSize != '' ) {
10241          styleNavigation=' style="font-size:'+G.O.navigationFontSize+';"';
10242        }      
10243        G.$E.conNavigationBar = jQuery('<div class="nGY2Navigationbar" '+styleNavigation+'></div>').appendTo(G.$E.base);
10244
10245        // pre-loader
10246        G.$E.conLoadingB = jQuery('<div class="nanoGalleryLBarOff"><div></div><div></div><div></div><div></div><div></div></div>').appendTo(G.$E.base);
10247
10248        // gallery
10249        G.$E.conTnParent = jQuery('<div class="nGY2Gallery"></div>').appendTo( G.$E.base );
10250        G.$E.conTn = jQuery('<div class="nGY2GallerySub"></div>').appendTo( G.$E.conTnParent );
10251
10252        // configure gallery
10253        switch( G.O.thumbnailAlignment ) {
10254          case 'left':
10255            G.$E.conTnParent.css({'text-align':'left'});
10256            // G.$E.conNavBCon.css({'margin-left':0 });
10257            break;
10258          case 'right':
10259            G.$E.conTnParent.css({'text-align':'right'});
10260            // G.$E.conNavBCon.css({ 'margin-right':0});
10261            break;
10262        }
10263        
10264        // apply galleryBuildInit2 css settings to the gallery
10265        if( G.O.galleryBuildInit2 !== undefined ) {
10266        var t1=G.O.galleryBuildInit2.split('|');
10267          for( var i=0; i<t1.length; i++ ) {
10268            var o1=t1[i].split('_');
10269            if( o1.length == 2 ) {
10270              G.$E.conTn.css(o1[0], o1[1]);
10271            }
10272          }
10273        }
10274        
10275        // configure gallery depending on some thumbnail hover effects
10276        var effects=G.tn.hoverEffects.std.concat(G.tn.hoverEffects.level1);
10277        for( var j=0; j<effects.length; j++) {
10278          switch( effects[j].type ) {
10279            case 'scale':
10280            case 'rotateZ':
10281            case 'rotateX':
10282            case 'rotateY':
10283            case 'translateX':
10284            case 'translateY':
10285              // handle some special cases
10286              if( effects[j].element == '.nGY2GThumbnail' ) {
10287                // allow thumbnail upscale over the gallery's aera
10288                G.$E.base.css('overflow', 'visible');
10289                G.$E.base.find('.nGY2GallerySub').css('overflow', 'visible');
10290                G.$E.conTnParent.css('overflow', 'visible');
10291              }
10292              break;
10293          }
10294        }
10295        
10296        // Gallery bottom container
10297        G.$E.conTnBottom = jQuery('<div class="nGY2GalleryBottom" '+styleNavigation+'></div>').appendTo( G.$E.conTnParent );
10298        
10299        // portable edition
10300        if( G.O.portable ) {
10301          // http://www.picresize.com/
10302          // http://base64encode.net/base64-image-encoder
10303          // var logo='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAWCAYAAAA4oUfxAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAAB3RJTUUH4QMPBwY6mxZgsAAABTFJREFUSMe1ll9oVGcaxn/fd86ZSWbSkEBMiWNdTTfRxiVbXFiU1bjKGqNexlURKys0tHqXpQZ64Sq4FxKqFy4qFSm9kA1FHNhFISgJqFCd6lL/YC7M3jhrJv5JmGSSMzPnzDnfuxdpZtP4b1vaF154P3gPD+/zPC/nVSKiAQOsBj7O5XK/nZiYeEtELH6iUEqFNTU1U9XV1d8AnwNfA1qJCMCfHz169NcjR45UXL16VWWzWQnD0PxU4JZl6draWtXW1iYHDx4sLlmy5C/AZwRB0JVOpyWRSHhACMjPmOHChQuL6XRagiDoUiIyumvXrpq+vr6obduqs7OTjRvbsbSFUgqUgKjyFG5+mlKpVH6LCMYYRAQRQSmF1hqtNd+xijGGVCpFMpkkCALZuXOn19fXN6Gmp6dNc3NzMDo66nR2dnL+/Hm+Ov933PwUAPHKagqei4gBFNs7dxGPx38U/du2bSOZTNLQ0FB6+PChbWez2WI+n3dEhI3tf+Det0N8de0Imz9YQWHa48u/3afjgxbqEpUM/es/uF8W+fijffi+TywWQ0S4fv06t2/fJpfLsXjxYtauXUtTUxNBECAihGFIJBJh1apVXLhwgXw+r7LZbNGeYU7MLD1BEPCLxkWs+HUT+SmPJY0TvPerd6l/J05YcLCGHWzbxrZtHjx4wP79+7l27dr3Jqyurqarq4ujR49i2zYAWmvCMJyVygCiZ7dh9kOtNb5XopD3KBQ8fL9EseBRyHsUCz6zS3Dnzh3WrVtXBq6oqGDBggUA5HI5jh07xo4dOzDmf0ujVBlGAWjmhTGC41hEow6RiI3j2DgRh0jUxonYWJaFGGHPnj2Mj49jWRYHDhzg7t27DA0NMTAwwOrVqwFIJpOcOHECx3Fe6oEXwG3bYux5ltHHz3mSGePpk+c8yczUI+knVFVVcePmDe7fvw9AT08Pvb29NDc3U1dXx4YNG7h8+TItLS1orTl58iT5fL68Ga8En55yWb6iifff/iPD/0iQGfglG3/zJ6a+beHf/3yH6Mjv+P269Vy5cgWlFDU1NXR3dxOGYdlcnudRVVXFvn37MMaQTqcZHh5+Kbg99zHjSodPuj997cqMjY0hItTW1hKPx9FalzW1LIswDFm0aBEAQRDguu6bJ581hOd5GBNiTEgYhuXa8z1EhIaGBgAymQzpdBqlFKVSiTCc6bcsi5s3bwJQWVlJfX39fMO9XHMAy7LQeibn1o7toJSio6MDAN/36e7uxvd9IpEIlmURjUZJpVKcOXMGpRStra0sXbr0peDfo30+LS+4U2uMMaxcuZLdu3dz7tw5+vv7aWtrY+/evdTX13Pr1i1OnTrF5OQkAIcPH8ayrNeCvx51njTGGE6fPk0mk2FwcJBUKkUqlXqh9/jx42zatKnMzJzhBEArpZT+zjGWZSEiBEHwypzVtbKykosXL3Lo0CEaGxvLpovFYqxZs4ZLly6VJQnDEBEpM6C11kopheu6JpFI+Fpr2bJli/zYGBkZkeHhYZmcnHxlz9atW0VrLYlEwndd19ixWOzx5s2b3z579qzp7+/X7e3ttLa2Yox5QaP5MfenEY1G0VoTBAHFYhFjTJlJrTX37t1jYGAAY4zp6OiQWCz2mCAItj979kyWL1/uAwE/7zERLFu2zH/69KkEQbB99ozaOz4+fqy3t7d2cHAwdF1XKaXe6P7/16AiQjwel/Xr1+uenp6Jurq6T4Av1JwD8j3gQ2BVsVh8S72J8x8QIiIVFRVTQAo4CwwB+r93qCLI9wKZ8AAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNy0wMy0xNVQwNzowNjo1OC0wNDowMBNQsyUAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTctMDMtMTVUMDc6MDY6NTgtMDQ6MDBiDQuZAAAAAElFTkSuQmCC';
10304          var logo = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAYCAYAAACbU/80AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAAB3RJTUUH4QgDBCAWVVC/hwAABRxJREFUSMetll9oVFcexz/nnDvJRBmSzWTrmD9uNGZsHta0/qFIFQTxRcnCBgTFNlX0YR8W+1AK9lGwCBJYgn0KKr5136S4gpUQTR4caJRslcxYWV3iaphQapJJppO5957z60Mmk4mN1q75wg/OPefc+/v9vt/fueenKEFEqICqsNWAVNiCA7XwaS0iZeejo6OIiCltdIBdJXMLOYp5/PjxsoTVS5nr0mYDJIE/lObeBhaYAn4oJbboAwBvBedHJicnPx8YGGh/8eJF1dvKoJSShoYGf//+/Zl4PP4l8M2yIEoSLErx6c2bN6W1tXVRglWzLVu2SCqVEhE5LiI457SIoEREW2udMaZtcnLy+2QyWZ3L5XRHR4f+4MNdoBUahUJhcWilmZ/NE4ZhOQHn3LIi1lqjtS6vjY6O8uTJE9vc3MyDBw+mYrHYn0Uk63me8gCtlHLA7uHh4bW5XC7oePddPTQ8xHffDjM/PYe3thqMws35iAcHPj5ENBp9Yxmy2Sw7d+40z549C+7du9ewb9++D6y13wDaK+kE0DAzMyNKKbXtvfd5EfzM+Ef/4C+8x23+wzPm+IhtfMf3/Ksuyl+7u9FaY63l+vXrpFIpCoUCmzdvpquri9bWVoIgQClFIpFg48aNPH/+XE9NTQkQLTGmvEXKRERprZWIEIYhQRjQbN6hmUb+tCaPNnM055v40f3If7XBGMPT8af0fNLD0NDQsozPnDlDb28vx44dIwxDRARrLSKCKmUbiUQQkWWnoLJ20UpjFYAjVA6rBJTFV5ZIJIIfBBw4eICxsTHq6uo4dOgQ8XicgYEB7t69y/Hjx4nH43R1dVHB8q+w4hlXSmGd5edwmjCco5DLkZ+aJvTnyIdTrFmzhn9+/TVjY2M0NTVx+/Zt+vv7OXfuHKlUip6eHgBOnz6N7/vlYl0JKzIw78/T+sdGbn6yjf5ZS2HtJgIP+mcC5kySI1uSXPjqAlprTp06RWdnJ8ViEaUUVVVVnD9/nqtXr5LJZHj48CFbt279fQEEYUisZi2fXel9bWU750gmkwRBgNYaz/Ow1lJfX088Hmd2dpZcLvdaBl4pgQChH4B1iHU4a8E6Qj9ARGhpaUFrzeDgIJFIBGMM1lqMMWQyGSYmJohEIqxfv/7314CIoADtGTAaZTTaLI2VUhw+fBjnHBcvXuTy5cs45/A8j3Q6zcmTJ/F9n71799LW1rbgSOs3D+B1lBljcM7R3d3N0aNHKRQKnDhxgs7OTnbt2sX27dsZGRkhHo/T19e3+Kt/fQ1YawFwzolSCs/zUEqVtX1VcJcuXSKRSNDf3086nS6v79mzh76+Pjo6OigWi1RXV2OMWZC29PL8/PxSAL7vE41Gf4rFYkpEePToEb7vU1VVxW+ht7eXs2fPcv/+fQqFAps2baKlpaW8Xl1dTS6XY3x8HBFxtbW1BiiW4hAlInp8fNxt2LChPZvN/ru9vT2Sz+e93bt3qx07diwrzJWYcM5RU1NDNBots5bP53HOlS+kO3fuMDIy4hKJhKTT6ena2tqtxWJxoqamRr98HX9x7do1qaurExYaiXCVzK5bt04GBwdFRP728nVcWZAO+Hsmk/nsxo0bTTMzM5FXHZ83hYhQX1/vHzx48H9tbW1ngSsVvpYCmJ2dJRaLKRbapjpgOxB7K+9LmAbuAnOAnpiYcI2NjUsRLlo2myUMQ1M5t5rmnDO3bt1aNlfmd4W2XL/0/H8pUDF2rNCW/wLRuCkxx8V6wgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNy0wOC0wM1QwNDozMjoyMi0wNDowMO7mdkwAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTctMDgtMDNUMDQ6MzI6MjItMDQ6MDCfu87wAAAAAElFTkSuQmCC';
10305          var st = "font-weight:bold !important;color: #FF0075 !important;font-size: 14px !important;text-transform: lowercase !important;cursor:pointer !important;text-align: center !important;
vendor: 67,815 bytes, lines 10305-12543
10305Text-Shadow: #000000 1px 0px 0px, #000000 1px 1px 0px, #000000 1px -1px 0px, #000000 -1px 1px 0px, #000000 -1px 0px 0px, #000000 -1px -1px 0px, #000000 0px 1px 0px, #000000 0px -1px 0px !important;";
10306          G.$E.ngy2i=jQuery('<div class="nGY2PortInfo"><a href="http://nano.gallery" target="_blank" title="nanogallery2 | easy photo gallery for your website" style="' + st + '"><img src="' + logo + '" style="height:32px !important;width:initial !important;box-shadow: none !important;vertical-align: middle !important;"/> &nbsp; nanogallery2</a></div>').appendTo(G.$E.base);
10307          
10308          G.$E.ngy2i.find('a').on({
10309            mouseenter: function () {
10310              jQuery(this).attr('style', st);
10311            },
10312            mouseleave: function () {
10313              jQuery(this).attr('style', st);
10314            }
10315          });
10316        }
10317      }
10318      
10319      // Error console
10320      G.$E.conConsole = jQuery('<div class="nGY2ConsoleParent"></div>').appendTo(G.$E.base);
10321
10322      // i18n translations
10323      i18n();
10324
10325      if( !G.O.lightboxStandalone ) {
10326        // cache some thumbnails data (sizes, styles...)
10327        ThumbnailDefCaches();
10328
10329        // do special settings depending for some options
10330        // thumbnail display transition
10331        switch( G.tn.opt.Get('displayTransition') ) {
10332          case 'SCALEDOWN':
10333          case 'RANDOMSCALE':
10334          default:
10335            G.$E.base.css('overflow', 'visible');
10336            G.$E.conTnParent.css('overflow', 'visible');
10337            G.$E.conTn.css('overflow', 'visible');
10338            break;
10339        }
10340      }
10341      
10342    }
10343    
10344    function TriggerCustomEvent ( eventName ) {
10345      // G.$E.base.trigger('pageChanged.nanogallery2', new Event('pageChanged.nanogallery2'));
10346      var eN = eventName + '.nanogallery2';
10347      var event=null;
10348      try {
10349          event = new Event( eN );
10350        } catch(e) {
10351          event = document.createEvent('Event');
10352          event.initEvent(eN, false, false);
10353        }
10354        G.$E.base.trigger(eN, event);
10355    }
10356    
10357    
10358    /** @function SetGlobalEvents */
10359    function SetGlobalEvents() {
10360      // GLOBAL EVENT MANAGEMENT
10361
10362      if( !G.O.lightboxStandalone ) {
10363        G.$E.conTnParent.on({
10364          mouseenter: GalleryMouseEnter,
10365          mouseleave: GalleryMouseLeave
10366        }, ".nGY2GThumbnail");    //pass the element as an argument to .on
10367      
10368        // G.GOM.hammertime = new NGHammer(G.$E.conTn[0], { touchAction: 'none' });
10369        G.GOM.hammertime = new NGHammer( G.$E.conTn[0] );
10370        // G.GOM.hammertime.domEvents = true;
10371      
10372        
10373        // PAN on gallery (pagination)
10374        G.GOM.hammertime.on('pan', function(ev) {
10375          if( !G.VOM.viewerDisplayed ) {
10376            if( G.O.paginationSwipe && G.layout.support.rows && G.galleryDisplayMode.Get() == 'PAGINATION' ) {
10377              if( Math.abs(ev.deltaY) > G.GOM.panThreshold ) {
10378                G.GOM.panYOnly = true;
10379              }
10380              if( !G.GOM.panYOnly ) {
10381                G.$E.conTn.css( G.CSStransformName , 'translate('+(ev.deltaX)+'px,0px)');
10382              }
10383            }
10384          }
10385        });
10386        G.GOM.hammertime.on('panend', function(ev) {
10387          if( !G.VOM.viewerDisplayed ) {
10388            if( G.O.paginationSwipe && G.layout.support.rows && G.galleryDisplayMode.Get() == 'PAGINATION' ) {
10389              if( !G.GOM.panYOnly ) {
10390                if( ev.deltaX > 50 ) {
10391                  paginationPreviousPage();
10392                  return;
10393                }
10394                if(  ev.deltaX < -50 ) {
10395                  paginationNextPage();
10396                  return;
10397                }
10398              }
10399              G.GOM.panYOnly = false;
10400              G.$E.conTn.css( G.CSStransformName , 'translate(0px,0px)');
10401              // pX=0;
10402            }
10403          }
10404        });
10405        // tap on gallery
10406        G.GOM.hammertime.on('tap', function(ev) {
10407          if( !G.VOM.viewerDisplayed ) {
10408            ev.srcEvent.stopPropagation();
10409            ev.srcEvent.preventDefault();  // cancel  mouseenter event
10410
10411            if( ev.pointerType == 'mouse') {
10412              if( GalleryClicked(ev.srcEvent) == 'exit' ) { return; }
10413            }
10414            else {
10415              var r = GalleryEventRetrieveElementl(ev.srcEvent, false);
10416              if( r.GOMidx == -1 ) { return; }
10417              if( r.action != 'NONE' && r.action != 'OPEN' ) {
10418                // toolbar touched --> execute action
10419                GalleryClicked(ev.srcEvent);
10420                return;
10421              }
10422              
10423              if( G.GOM.slider.hostIdx == r.GOMidx ) {
10424                // touch on thumbnail slider -> open immediately
10425                ThumbnailHoverOutAll();
10426                ThumbnailOpen(G.GOM.items[G.GOM.slider.currentIdx].thumbnailIdx, true);
10427                return;
10428              }
10429
10430              if( (G.GOM.curNavLevel == 'l1' && G.O.touchAnimationL1 == false) ||  (G.GOM.curNavLevel == 'lN' && G.O.touchAnimation == false) ) {
10431                // open on single touch (no hover animation)
10432                ThumbnailOpen(G.GOM.items[r.GOMidx].thumbnailIdx, true);
10433                return;
10434              }
10435              
10436              if( G.O.touchAutoOpenDelay > 0 ) {
10437                // open on single touch after end of hover animation (=defined delay)
10438                ThumbnailHoverOutAll();
10439                ThumbnailHover( r.GOMidx );
10440                window.clearInterval( G.touchAutoOpenDelayTimerID );
10441                G.touchAutoOpenDelayTimerID = window.setInterval(function(){
10442                  window.clearInterval( G.touchAutoOpenDelayTimerID );
10443                  ThumbnailOpen( G.GOM.items[r.GOMidx].thumbnailIdx, true );
10444                }, G.O.touchAutoOpenDelay );
10445              }
10446              else {
10447                // two touch scenario
10448                if( !G.I[G.GOM.items[r.GOMidx].thumbnailIdx].hovered ) {
10449                  ThumbnailHoverOutAll();
10450                  ThumbnailHover(r.GOMidx);
10451                }
10452                else {
10453                  // second touch
10454                  ThumbnailOpen(G.GOM.items[r.GOMidx].thumbnailIdx, true);
10455                }
10456              }
10457            }
10458          }
10459        });
10460      
10461        // browser location hash management
10462        if( G.O.locationHash ) {
10463          // jQuery(window).bind( 'hashchange', function() {
10464            // ProcessLocationHash();
10465          // });
10466          jQuery(window).on('hashchange.nanogallery2.' + G.baseEltID, function() {ProcessLocationHash();} );
10467        }
10468      }
10469      
10470      // Page resize / orientation change
10471      jQuery(window).on('resize.nanogallery2.' + G.baseEltID + ' orientationChange.nanogallery2.' + G.baseEltID, debounce( ResizeWindowEvent, G.O.eventsDebounceDelay, false) );
10472      
10473      // Event page scrolled
10474      jQuery(window).on('scroll.nanogallery2.' + G.baseEltID, debounce( OnScrollEvent, G.O.eventsDebounceDelay, false) );
10475
10476      if( !G.O.lightboxStandalone ) {
10477        // Scroll event on first scrollable parent element
10478        G.$E.scrollableParent = getScrollableParent( G.$E.base[0] );
10479        var sp = getScrollableParent( G.$E.base[0] );
10480        if( sp !== null ) {
10481          G.$E.scrollableParent = jQuery( sp );
10482          G.$E.scrollableParent.on('scroll.nanogallery2.' + G.baseEltID, debounce( OnScrollEvent, G.O.eventsDebounceDelay, false) );
10483        }
10484      }
10485			
10486      // lightbox: hide tools/gallery after defined delay
10487      G.VOM.toolsHide = debounce( ViewerToolsHide, G.O.viewerHideToolsDelay, false );
10488      
10489      // Keyboard management
10490      jQuery(document).keyup(function(e) {
10491        if( G.popup.isDisplayed ) {
10492          switch( e.keyCode) {
10493            case 27:    // Esc key
10494              G.popup.close();
10495              break;
10496          }
10497        }
10498        else {
10499          if( G.VOM.viewerDisplayed ) {
10500            ViewerToolsUnHide();
10501            switch( e.keyCode) {
10502              case 27:    // Escape key
10503              case 40:    // DOWN
10504              case 38:    // UP
10505                LightboxClose();
10506                break;
10507              case 32:    // SPACE
10508              case 13:    // ENTER
10509                SlideshowToggle();
10510                break;
10511              case 39:    // RIGHT
10512              case 33:    // PAGE UP
10513                DisplayNextMedia();
10514                break;
10515              case 37:    // LEFT
10516              case 34:    // PAGE DOWN
10517                DisplayPreviousMedia();
10518                break;
10519              case 35:    // END
10520              case 36:    // BEGIN
10521            }
10522          }
10523        }
10524      });
10525      
10526      // mouse wheel to zoom in/out the image displayed in the internal lightbox
10527      jQuery(window).bind('mousewheel wheel', function(e){
10528
10529        if( G.VOM.viewerDisplayed && G.VOM.content.current.NGY2Item().mediaKind == 'img' ) {
10530
10531          var deltaY = 0;
10532          e.preventDefault();
10533
10534          if( ViewerZoomStart() ) {
10535            if (e.originalEvent.deltaY) { // FireFox 17+ (IE9+, Chrome 31+?)
10536              deltaY = e.originalEvent.deltaY;
10537            } else if (e.originalEvent.wheelDelta) {
10538              deltaY = -e.originalEvent.wheelDelta;
10539            }
10540            ViewerZoomIn( deltaY <= 0 ? true : false );
10541          }
10542        }
10543      });
10544      
10545      // mouse move -> unhide lightbox toolbars
10546      jQuery(window).bind('mousemove', function(e){
10547        if( G.VOM.viewerDisplayed ) {
10548					if( G.VOM.toolbarsDisplayed == false ) {
10549						G.VOM.singletapTime = new Date().getTime();		// to avoid conflict with SINGLETAP event
10550						debounce( ViewerToolsUnHide, 100, false )();
10551					}
10552        }
10553      });
10554      
10555      // fullscreen mode on/off --> internal lightbox
10556      if( ngscreenfull.enabled ) {
10557        // ngscreenfull.onchange(() => {
10558        ngscreenfull.onchange( function() {
10559          if( G.VOM.viewerDisplayed ) {
10560            if( ngscreenfull.isFullscreen ) {
10561              G.VOM.viewerIsFullscreen=true;
10562              G.VOM.$viewer.find('.fullscreenButton').html(G.O.icons.viewerFullscreenOff);
10563            }
10564            else {
10565              G.VOM.viewerIsFullscreen=false;
10566              G.VOM.$viewer.find('.fullscreenButton').html(G.O.icons.viewerFullscreenOn);
10567            }
10568          }
10569        });
10570      }
10571
10572    }
10573    
10574		
10575    //----- Manage browser location hash (deep linking and browser back/forward)
10576    function ProcessLocationHash() {
10577
10578      // standard use case -> location hash processing
10579      if( !G.O.locationHash ) { return false; }
10580
10581      var curGal = '#nanogallery/' + G.baseEltID + '/',
10582      newLocationHash = location.hash;
10583      if( G.O.debugMode ) {
10584        console.log('------------------------ PROCESS LOCATION HASH');
10585        console.log('newLocationHash1: ' +newLocationHash);
10586        console.log('G.locationHashLastUsed: ' +G.locationHashLastUsed);
10587      }
10588      
10589      if( newLocationHash == '' ) {
10590        // if( G.GOM.lastDisplayedIdx != -1 ) {
10591        if( G.locationHashLastUsed !== '' ) {
10592          // back button and no hash --> display first album
10593          if( G.O.debugMode ) { console.log('display root album'  ); }
10594          G.locationHashLastUsed = '';
10595          if( G.O.debugMode ) { console.log('new3 G.locationHashLastUsed: ' + G.locationHashLastUsed); }
10596          DisplayAlbum('', '0');
10597          return true;
10598        }
10599      }
10600
10601      if( newLocationHash == G.locationHashLastUsed ) { return; }
10602      
10603      if( newLocationHash.indexOf(curGal) == 0 ) {
10604        // item IDs detected
10605        var IDs=parseIDs( newLocationHash.substring(curGal.length) );
10606        if( IDs.imageID != '0' ) {
10607          if( G.O.debugMode ) { console.log('display image: ' + IDs.albumID +'-'+ IDs.imageID ); }
10608          DisplayPhoto( IDs.imageID, IDs.albumID );
10609          return true;
10610        }
10611        else {
10612          if( G.O.debugMode ) { console.log('display album: ' + IDs.albumID  ); }
10613          DisplayAlbum( '-1', IDs.albumID );
10614          return true;
10615        }
10616      }    
10617      
10618      return false;
10619    }
10620
10621    //---- Set a new browser location hash
10622    function SetLocationHash(albumID, imageID ) {
10623      if( !G.O.locationHash || G.O.lightboxStandalone ) { return false; }
10624
10625      if( G.O.debugMode ) {
10626        console.log('------------------------ SET LOCATION HASH');
10627      }
10628      
10629      if( imageID == '' && (albumID == '-1' || albumID == '0' || G.O.album == albumID ) ) {
10630        // root album level --> do not set top.location.hash if not already set
10631        if( location.hash != '' ) {
10632          // try to clear the hash if set
10633          if ("pushState" in history) {
10634            history.pushState("", document.title, window.location.pathname + window.location.search);
10635          }
10636          else {
10637            location.hash='';
10638          }
10639        }
10640        G.locationHashLastUsed='';
10641        if( G.O.debugMode ) { console.log('new2 G.locationHashLastUsed: '+G.locationHashLastUsed); }
10642        return;
10643      }
10644      
10645      var newLocationHash='#'+'nanogallery/'+G.baseEltID+'/'+ albumID;
10646      if( imageID != '' ) {
10647        newLocationHash+='/'+imageID;
10648      }
10649
10650      var lH=location.hash;
10651      if( G.O.debugMode ) {
10652        console.log('newLocationHash2: '+newLocationHash);
10653        console.log('location.hash: '+lH);
10654      }
10655
10656      G.locationHashLastUsed=newLocationHash;
10657      if( G.O.debugMode ) { console.log('new G.locationHashLastUsed: '+G.locationHashLastUsed); }
10658      
10659      if(  lH == '' || lH != newLocationHash ) {
10660        // G.locationHashLastUsed='#'+newLocationHash;
10661        try {
10662          top.location.hash=newLocationHash;
10663        }
10664        catch(e) {
10665          // location hash is not supported by current browser --> disable the option
10666          G.O.locationHash=false;
10667        }
10668      }
10669    }
10670    
10671    
10672		// WINDOW RESIZE EVENT
10673    function ResizeWindowEvent() {
10674      CacheViewport();
10675
10676			var l = G.GOM.curNavLevel;
10677			var w = G.GOM.curWidth;
10678			
10679      if( G.VOM.viewerDisplayed ) {
10680				// lightbox
10681        ResizeLightbox();
10682        G.VOM.gallery.Resize();
10683      }
10684      else {
10685				// gallery
10686        if( G.galleryResizeEventEnabled ) {
10687          var nw = RetrieveCurWidth();
10688
10689					if( G.GOM.albumIdx != -1 ) {
10690
10691						// check if the gallery needs to be rendered again because the width changed
10692						
10693            var s = G.tn.settings;
10694						if( G.layout.engine == "MOSAIC") {
10695							// Mosaic layout
10696							if( JSON.stringify(s.mosaic[l][w]) !== JSON.stringify(s.mosaic[l][nw]) ) {
10697								// mosaic definition changed
10698								G.GOM.curWidth = nw;
10699								G.GOM.pagination.currentPage = 0;
10700								GalleryRender( G.GOM.albumIdx );
10701                return;
10702							}
10703						}
10704						else {
10705							// other layouts
10706							if( s.height[l][w] != s.height[l][nw] || s.width[l][w] != s.width[l][nw] || s.gutterHeight[l][w] != s.gutterHeight[l][nw]  || s.gutterWidth[l][w] != s.gutterWidth[l][nw]  ) {
10707								// thumbnail size / gutter size changed --> render the gallery with the new values
10708								G.GOM.curWidth = nw;
10709								//G.layout.SetEngine();
10710								G.GOM.pagination.currentPage = 0;
10711								GalleryRender( G.GOM.albumIdx );
10712                return;
10713							}
10714						}
10715						// return;
10716          }
10717          // else {
10718            GalleryResize();
10719          // }
10720        }
10721      }
10722    }
10723    
10724     
10725
10726		// SCROLL EVENT -> on WINDOW or SCROLLABLE PARENT CONTAINER
10727    function OnScrollEvent() {
10728			if( !G.VOM.viewerDisplayed ) {
10729        GalleryResizeOnScrollEvent();
10730      }
10731    }
10732		
10733    // the gallery may currently be refreshed, so ensure that at the end of the refresh, the gallery is refreshed again because the page may have been scrolled in the meantime
10734    function GalleryResizeOnScrollEvent() {
10735      if( G.galleryResizeEventEnabled == false) {
10736        window.setTimeout(GalleryResizeOnScrollEvent, 10);  // check again in 10ms
10737      } else {
10738        GalleryResize();
10739      }
10740    }
10741
10742
10743    
10744    // I18N : define text translations
10745    function i18n() {
10746
10747      // browser language
10748      G.i18nLang = (navigator.language || navigator.userLanguage).toUpperCase();
10749      if( G.i18nLang === 'UNDEFINED') { G.i18nLang=''; }
10750
10751      var llang=-('_'+G.i18nLang).length;
10752      
10753      if( toType(G.O.i18n) == 'object' ){
10754      
10755        for( var key in G.O.i18n ) {
10756          //var value = G.O.i18n[key];
10757          var s=key.substr(llang);
10758          if( s == ('_'+G.i18nLang) ) {
10759            G.i18nTranslations[key.substr(0,key.length-s.length)]=G.O.i18n[key];
10760          } 
10761          else {
10762            G.i18nTranslations[key]=G.O.i18n[key];
10763          }
10764        }
10765      }
10766    }
10767
10768    function GetI18nItem( item, property ) {
10769      var s='';
10770      if( G.i18nLang != '' ) {
10771        if( item[property+'_'+G.i18nLang] !== undefined && item[property+'_'+G.i18nLang].length>0 ) {
10772          s=item[property+'_'+G.i18nLang];
10773          return s;
10774        }
10775      }
10776      s=item[property];
10777      return s;
10778    }
10779
10780    
10781    function RetrieveCurWidth() {
10782      var vpW = G.GOM.cache.viewport.w;
10783      
10784      if( G.O.breakpointSizeSM > 0 && vpW < G.O.breakpointSizeSM) { return 'xs'; }
10785      if( G.O.breakpointSizeME > 0 && vpW < G.O.breakpointSizeME) { return 'sm'; }
10786      if( G.O.breakpointSizeLA > 0 && vpW < G.O.breakpointSizeLA) { return 'me'; }
10787      if( G.O.breakpointSizeXL > 0 && vpW < G.O.breakpointSizeXL) { return 'la'; }
10788      
10789      return 'xl';
10790    }
10791
10792    
10793    /** @function browserNotification */
10794    function browserNotification() {
10795      var m = 'Your browser version is not supported anymore. The image gallery cannot be displayed. <br><br>Please update to a more recent one. Download:<br>';
10796      m    += '&nbsp;&nbsp;&nbsp; <a href="http://www.google.com/chrome/?hl=en-US)">Chrome</a><br>';
10797      m    += '&nbsp;&nbsp;&nbsp; <a href="http://www.mozilla.com/firefox/)">Firefox</a><br>';
10798      m    += '&nbsp;&nbsp;&nbsp; <a href="http://www.microsoft.com/windows/internet-explorer/default.aspx">Internet Explorer</a><br>';
10799      m    += '&nbsp;&nbsp;&nbsp; <a href="http://www.apple.com/safari/download/">Safari</a>';
10800      NanoAlert(G,  m, false);
10801    }
10802
10803    // Original author : John Hrvatin, Lead Program Manager, Internet Explorer - http://blogs.msdn.com/b/ie/archive/2011/10/28/a-best-practice-for-programming-with-vendor-prefixes.aspx
10804    function FirstSupportedPropertyName(prefixedPropertyNames) {
10805      var tempDiv = document.createElement("div");
10806      for (var i = 0; i < prefixedPropertyNames.length; ++i) {
10807        if (typeof tempDiv.style[prefixedPropertyNames[i]] != 'undefined')
10808          return prefixedPropertyNames[i];
10809      }
10810      return null;
10811    }
10812
10813    
10814    
10815    
10816  }
10817  
10818
10819
10820//##########################################################################################################################
10821//## imagesLoaded ##########################################################################################################
10822//##########################################################################################################################
10823
10824// external module EMBEDED in nanogallery
10825// NGY BUILD:
10826// replace "imagesLoaded" with "ngimagesLoaded"
10827// replace "ImagesLoaded" with "ngImagesLoaded"
10828// replace "EvEmitter" with "ngEvEmitter"
10829// replace "var $ = window.jQuery" with "var $ = jQuery;"
10830// 2x (global.ngEvEmitter and window.ngimagesLoaded = f...)ignore package manager and set browser global
10831
10832
10833
10834/*!
10835 * imagesLoaded PACKAGED v4.1.1
10836 * JavaScript is all like "You images are done yet or what?"
10837 * MIT License
10838 */
10839
10840/**
10841 * EvEmitter v1.0.3
10842 * Lil' event emitter
10843 * MIT License
10844 */
10845
10846 
10847/* jshint unused: true, undef: true, strict: true */
10848
10849( function( global, factory ) {
10850  // universal module definition
10851  /* jshint strict: false */ /* globals define, module, window */
10852//  if ( typeof define == 'function' && define.amd ) {
10853    // AMD - RequireJS
10854//    define( 'ev-emitter/ev-emitter',factory );
10855//  } else if ( typeof module == 'object' && module.exports ) {
10856    // CommonJS - Browserify, Webpack
10857//    module.exports = factory();
10858//  } else {
10859    // Browser globals
10860    global.ngEvEmitter = factory();
10861//  }
10862
10863}( typeof window != 'undefined' ? window : this, function() {
10864
10865
10866
10867function ngEvEmitter() {}
10868
10869var proto = ngEvEmitter.prototype;
10870
10871proto.on = function( eventName, listener ) {
10872  if ( !eventName || !listener ) {
10873    return;
10874  }
10875  // set events hash
10876  var events = this._events = this._events || {};
10877  // set listeners array
10878  var listeners = events[ eventName ] = events[ eventName ] || [];
10879  // only add once
10880  if ( listeners.indexOf( listener ) == -1 ) {
10881    listeners.push( listener );
10882  }
10883
10884  return this;
10885};
10886
10887proto.once = function( eventName, listener ) {
10888  if ( !eventName || !listener ) {
10889    return;
10890  }
10891  // add event
10892  this.on( eventName, listener );
10893  // set once flag
10894  // set onceEvents hash
10895  var onceEvents = this._onceEvents = this._onceEvents || {};
10896  // set onceListeners object
10897  var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || {};
10898  // set flag
10899  onceListeners[ listener ] = true;
10900
10901  return this;
10902};
10903
10904proto.off = function( eventName, listener ) {
10905  var listeners = this._events && this._events[ eventName ];
10906  if ( !listeners || !listeners.length ) {
10907    return;
10908  }
10909  var index = listeners.indexOf( listener );
10910  if ( index != -1 ) {
10911    listeners.splice( index, 1 );
10912  }
10913
10914  return this;
10915};
10916
10917proto.emitEvent = function( eventName, args ) {
10918  var listeners = this._events && this._events[ eventName ];
10919  if ( !listeners || !listeners.length ) {
10920    return;
10921  }
10922  var i = 0;
10923  var listener = listeners[i];
10924  args = args || [];
10925  // once stuff
10926  var onceListeners = this._onceEvents && this._onceEvents[ eventName ];
10927
10928  while ( listener ) {
10929    var isOnce = onceListeners && onceListeners[ listener ];
10930    if ( isOnce ) {
10931      // remove listener
10932      // remove before trigger to prevent recursion
10933      this.off( eventName, listener );
10934      // unset once flag
10935      delete onceListeners[ listener ];
10936    }
10937    // trigger listener
10938    listener.apply( this, args );
10939    // get next listener
10940    i += isOnce ? 0 : 1;
10941    listener = listeners[i];
10942  }
10943
10944  return this;
10945};
10946
10947return ngEvEmitter;
10948
10949}));
10950
10951/*!
10952 * ngimagesLoaded v4.1.1
10953 * JavaScript is all like "You images are done yet or what?"
10954 * MIT License
10955 */
10956
10957( function( window, factory ) { 'use strict';
10958  // universal module definition
10959
10960  /*global define: false, module: false, require: false */
10961
10962//  if ( typeof define == 'function' && define.amd ) {
10963    // AMD
10964//    define( [
10965//      'ev-emitter/ev-emitter'
10966//    ], function( ngEvEmitter ) {
10967//      return factory( window, ngEvEmitter );
10968//    });
10969//  } else if ( typeof module == 'object' && module.exports ) {
10970    // CommonJS
10971//    module.exports = factory(
10972//      window,
10973//      require('ev-emitter')
10974//    );
10975//  } else {
10976    // browser global
10977    window.ngimagesLoaded = factory(
10978      window,
10979      window.ngEvEmitter
10980    );
10981  //}
10982
10983})( window,
10984
10985// --------------------------  factory -------------------------- //
10986
10987function factory( window, ngEvEmitter ) {
10988
10989
10990
10991// var $ = window.jQuery;
10992var $ = jQuery;
10993var console = window.console;
10994
10995// -------------------------- helpers -------------------------- //
10996
10997// extend objects
10998function extend( a, b ) {
10999  for ( var prop in b ) {
11000    a[ prop ] = b[ prop ];
11001  }
11002  return a;
11003}
11004
11005// turn element or nodeList into an array
11006function makeArray( obj ) {
11007  var ary = [];
11008  if ( Array.isArray( obj ) ) {
11009    // use object if already an array
11010    ary = obj;
11011  } else if ( typeof obj.length == 'number' ) {
11012    // convert nodeList to array
11013    for ( var i=0; i < obj.length; i++ ) {
11014      ary.push( obj[i] );
11015    }
11016  } else {
11017    // array of single index
11018    ary.push( obj );
11019  }
11020  return ary;
11021}
11022
11023// -------------------------- ngimagesLoaded -------------------------- //
11024
11025/**
11026 * @param {Array, Element, NodeList, String} elem
11027 * @param {Object or Function} options - if function, use as callback
11028 * @param {Function} onAlways - callback function
11029 */
11030function ngImagesLoaded( elem, options, onAlways ) {
11031  // coerce ngImagesLoaded() without new, to be new ngImagesLoaded()
11032  if ( !( this instanceof ngImagesLoaded ) ) {
11033    return new ngImagesLoaded( elem, options, onAlways );
11034  }
11035  // use elem as selector string
11036  if ( typeof elem == 'string' ) {
11037    elem = document.querySelectorAll( elem );
11038  }
11039
11040  this.elements = makeArray( elem );
11041  this.options = extend( {}, this.options );
11042
11043  if ( typeof options == 'function' ) {
11044    onAlways = options;
11045  } else {
11046    extend( this.options, options );
11047  }
11048
11049  if ( onAlways ) {
11050    this.on( 'always', onAlways );
11051  }
11052
11053  this.getImages();
11054
11055  if ( $ ) {
11056    // add jQuery Deferred object
11057    this.jqDeferred = new $.Deferred();
11058  }
11059
11060  // HACK check async to allow time to bind listeners
11061  setTimeout( function() {
11062    this.check();
11063  }.bind( this ));
11064}
11065
11066ngImagesLoaded.prototype = Object.create( ngEvEmitter.prototype );
11067
11068ngImagesLoaded.prototype.options = {};
11069
11070ngImagesLoaded.prototype.getImages = function() {
11071  this.images = [];
11072
11073  // filter & find items if we have an item selector
11074  this.elements.forEach( this.addElementImages, this );
11075};
11076
11077/**
11078 * @param {Node} element
11079 */
11080ngImagesLoaded.prototype.addElementImages = function( elem ) {
11081  // filter siblings
11082  if ( elem.nodeName == 'IMG' ) {
11083    this.addImage( elem );
11084  }
11085  // get background image on element
11086  if ( this.options.background === true ) {
11087    this.addElementBackgroundImages( elem );
11088  }
11089
11090  // find children
11091  // no non-element nodes, #143
11092  var nodeType = elem.nodeType;
11093  if ( !nodeType || !elementNodeTypes[ nodeType ] ) {
11094    return;
11095  }
11096  var childImgs = elem.querySelectorAll('img');
11097  // concat childElems to filterFound array
11098  for ( var i=0; i < childImgs.length; i++ ) {
11099    var img = childImgs[i];
11100    this.addImage( img );
11101  }
11102
11103  // get child background images
11104  if ( typeof this.options.background == 'string' ) {
11105    var children = elem.querySelectorAll( this.options.background );
11106    for ( i=0; i < children.length; i++ ) {
11107      var child = children[i];
11108      this.addElementBackgroundImages( child );
11109    }
11110  }
11111};
11112
11113var elementNodeTypes = {
11114  1: true,
11115  9: true,
11116  11: true
11117};
11118
11119ngImagesLoaded.prototype.addElementBackgroundImages = function( elem ) {
11120  var style = getComputedStyle( elem );
11121  if ( !style ) {
11122    // Firefox returns null if in a hidden iframe https://bugzil.la/548397
11123    return;
11124  }
11125  // get url inside url("...")
11126  var reURL = /url\((['"])?(.*?)\1\)/gi;
11127  var matches = reURL.exec( style.backgroundImage );
11128  while ( matches !== null ) {
11129    var url = matches && matches[2];
11130    if ( url ) {
11131      this.addBackground( url, elem );
11132    }
11133    matches = reURL.exec( style.backgroundImage );
11134  }
11135};
11136
11137/**
11138 * @param {Image} img
11139 */
11140ngImagesLoaded.prototype.addImage = function( img ) {
11141  var loadingImage = new LoadingImage( img );
11142  this.images.push( loadingImage );
11143};
11144
11145ngImagesLoaded.prototype.addBackground = function( url, elem ) {
11146  var background = new Background( url, elem );
11147  this.images.push( background );
11148};
11149
11150ngImagesLoaded.prototype.check = function() {
11151  var _this = this;
11152  this.progressedCount = 0;
11153  this.hasAnyBroken = false;
11154  // complete if no images
11155  if ( !this.images.length ) {
11156    this.complete();
11157    return;
11158  }
11159
11160  function onProgress( image, elem, message ) {
11161    // HACK - Chrome triggers event before object properties have changed. #83
11162    setTimeout( function() {
11163      _this.progress( image, elem, message );
11164    });
11165  }
11166
11167  this.images.forEach( function( loadingImage ) {
11168    loadingImage.once( 'progress', onProgress );
11169    loadingImage.check();
11170  });
11171};
11172
11173ngImagesLoaded.prototype.progress = function( image, elem, message ) {
11174  this.progressedCount++;
11175  this.hasAnyBroken = this.hasAnyBroken || !image.isLoaded;
11176  // progress event
11177  this.emitEvent( 'progress', [ this, image, elem ] );
11178  if ( this.jqDeferred && this.jqDeferred.notify ) {
11179    this.jqDeferred.notify( this, image );
11180  }
11181  // check if completed
11182  if ( this.progressedCount == this.images.length ) {
11183    this.complete();
11184  }
11185
11186  if ( this.options.debug && console ) {
11187    console.log( 'progress: ' + message, image, elem );
11188  }
11189};
11190
11191ngImagesLoaded.prototype.complete = function() {
11192  var eventName = this.hasAnyBroken ? 'fail' : 'done';
11193  this.isComplete = true;
11194  this.emitEvent( eventName, [ this ] );
11195  this.emitEvent( 'always', [ this ] );
11196  if ( this.jqDeferred ) {
11197    var jqMethod = this.hasAnyBroken ? 'reject' : 'resolve';
11198    this.jqDeferred[ jqMethod ]( this );
11199  }
11200};
11201
11202// --------------------------  -------------------------- //
11203
11204function LoadingImage( img ) {
11205  this.img = img;
11206}
11207
11208LoadingImage.prototype = Object.create( ngEvEmitter.prototype );
11209
11210LoadingImage.prototype.check = function() {
11211  // If complete is true and browser supports natural sizes,
11212  // try to check for image status manually.
11213  var isComplete = this.getIsImageComplete();
11214  if ( isComplete ) {
11215    // report based on naturalWidth
11216    this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' );
11217    return;
11218  }
11219
11220  // If none of the checks above matched, simulate loading on detached element.
11221  this.proxyImage = new Image();
11222  this.proxyImage.addEventListener( 'load', this );
11223  this.proxyImage.addEventListener( 'error', this );
11224  // bind to image as well for Firefox. #191
11225  this.img.addEventListener( 'load', this );
11226  this.img.addEventListener( 'error', this );
11227  this.proxyImage.src = this.img.src;
11228};
11229
11230LoadingImage.prototype.getIsImageComplete = function() {
11231  return this.img.complete && this.img.naturalWidth !== undefined;
11232};
11233
11234LoadingImage.prototype.confirm = function( isLoaded, message ) {
11235  this.isLoaded = isLoaded;
11236  this.emitEvent( 'progress', [ this, this.img, message ] );
11237};
11238
11239// ----- events ----- //
11240
11241// trigger specified handler for event type
11242LoadingImage.prototype.handleEvent = function( event ) {
11243  var method = 'on' + event.type;
11244  if ( this[ method ] ) {
11245    this[ method ]( event );
11246  }
11247};
11248
11249LoadingImage.prototype.onload = function() {
11250  this.confirm( true, 'onload' );
11251  this.unbindEvents();
11252};
11253
11254LoadingImage.prototype.onerror = function() {
11255  this.confirm( false, 'onerror' );
11256  this.unbindEvents();
11257};
11258
11259LoadingImage.prototype.unbindEvents = function() {
11260  this.proxyImage.removeEventListener( 'load', this );
11261  this.proxyImage.removeEventListener( 'error', this );
11262  this.img.removeEventListener( 'load', this );
11263  this.img.removeEventListener( 'error', this );
11264};
11265
11266// -------------------------- Background -------------------------- //
11267
11268function Background( url, element ) {
11269  this.url = url;
11270  this.element = element;
11271  this.img = new Image();
11272}
11273
11274// inherit LoadingImage prototype
11275Background.prototype = Object.create( LoadingImage.prototype );
11276
11277Background.prototype.check = function() {
11278  this.img.addEventListener( 'load', this );
11279  this.img.addEventListener( 'error', this );
11280  this.img.src = this.url;
11281  // check if image is already complete
11282  var isComplete = this.getIsImageComplete();
11283  if ( isComplete ) {
11284    this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' );
11285    this.unbindEvents();
11286  }
11287};
11288
11289Background.prototype.unbindEvents = function() {
11290  this.img.removeEventListener( 'load', this );
11291  this.img.removeEventListener( 'error', this );
11292};
11293
11294Background.prototype.confirm = function( isLoaded, message ) {
11295  this.isLoaded = isLoaded;
11296  this.emitEvent( 'progress', [ this, this.element, message ] );
11297};
11298
11299// -------------------------- jQuery -------------------------- //
11300
11301ngImagesLoaded.makeJQueryPlugin = function( jQuery ) {
11302  jQuery = jQuery || window.jQuery;
11303  if ( !jQuery ) {
11304    return;
11305  }
11306  // set local variable
11307  $ = jQuery;
11308  // $().ngimagesLoaded()
11309  $.fn.ngimagesLoaded = function( options, callback ) {
11310    var instance = new ngImagesLoaded( this, options, callback );
11311    return instance.jqDeferred.promise( $(this) );
11312  };
11313};
11314// try making plugin
11315ngImagesLoaded.makeJQueryPlugin();
11316
11317// --------------------------  -------------------------- //
11318
11319return ngImagesLoaded;
11320
11321});
11322
11323
11324
11325//##########################################################################################################################
11326//## screenfull.js #########################################################################################################
11327//##########################################################################################################################
11328
11329// screenfull.js
11330// v4.0.1
11331// by sindresorhus - https://github.com/sindresorhus
11332// from: https://github.com/sindresorhus/screenfull.js
11333
11334// external module embeded in nanogallery
11335// NGY BUILD:
11336// replace "screenfull" with "ngscreenfull"
11337// 
11338
11339(function () {
11340	'use strict';
11341
11342	var document = typeof window !== 'undefined' && typeof window.document !== 'undefined' ? window.document : {};
11343	var isCommonjs = typeof module !== 'undefined' && module.exports;
11344	var keyboardAllowed = typeof Element !== 'undefined' && 'ALLOW_KEYBOARD_INPUT' in Element;
11345
11346	var fn = (function () {
11347		var val;
11348
11349		var fnMap = [
11350			[
11351				'requestFullscreen',
11352				'exitFullscreen',
11353				'fullscreenElement',
11354				'fullscreenEnabled',
11355				'fullscreenchange',
11356				'fullscreenerror'
11357			],
11358			// New WebKit
11359			[
11360				'webkitRequestFullscreen',
11361				'webkitExitFullscreen',
11362				'webkitFullscreenElement',
11363				'webkitFullscreenEnabled',
11364				'webkitfullscreenchange',
11365				'webkitfullscreenerror'
11366
11367			],
11368			// Old WebKit (Safari 5.1)
11369			[
11370				'webkitRequestFullScreen',
11371				'webkitCancelFullScreen',
11372				'webkitCurrentFullScreenElement',
11373				'webkitCancelFullScreen',
11374				'webkitfullscreenchange',
11375				'webkitfullscreenerror'
11376
11377			],
11378			[
11379				'mozRequestFullScreen',
11380				'mozCancelFullScreen',
11381				'mozFullScreenElement',
11382				'mozFullScreenEnabled',
11383				'mozfullscreenchange',
11384				'mozfullscreenerror'
11385			],
11386			[
11387				'msRequestFullscreen',
11388				'msExitFullscreen',
11389				'msFullscreenElement',
11390				'msFullscreenEnabled',
11391				'MSFullscreenChange',
11392				'MSFullscreenError'
11393			]
11394		];
11395
11396		var i = 0;
11397		var l = fnMap.length;
11398		var ret = {};
11399
11400		for (; i < l; i++) {
11401			val = fnMap[i];
11402			if (val && val[1] in document) {
11403				for (i = 0; i < val.length; i++) {
11404					ret[fnMap[0][i]] = val[i];
11405				}
11406				return ret;
11407			}
11408		}
11409
11410		return false;
11411	})();
11412
11413	var eventNameMap = {
11414		change: fn.fullscreenchange,
11415		error: fn.fullscreenerror
11416	};
11417
11418	var ngscreenfull = {
11419		request: function (elem) {
11420			return new Promise(function (resolve) {
11421				var request = fn.requestFullscreen;
11422
11423				var onFullScreenEntered = function () {
11424					this.off('change', onFullScreenEntered);
11425					resolve();
11426				}.bind(this);
11427
11428				elem = elem || document.documentElement;
11429
11430				// Work around Safari 5.1 bug: reports support for
11431				// keyboard in fullscreen even though it doesn't.
11432				// Browser sniffing, since the alternative with
11433				// setTimeout is even worse.
11434				if (/ Version\/5\.1(?:\.\d+)? Safari\//.test(navigator.userAgent)) {
11435					elem[request]();
11436				} else {
11437					elem[request](keyboardAllowed ? Element.ALLOW_KEYBOARD_INPUT : {});
11438				}
11439
11440				this.on('change', onFullScreenEntered);
11441			}.bind(this));
11442		},
11443		exit: function () {
11444			return new Promise(function (resolve) {
11445				if (!this.isFullscreen) {
11446					resolve();
11447					return;
11448				}
11449
11450				var onFullScreenExit = function () {
11451					this.off('change', onFullScreenExit);
11452					resolve();
11453				}.bind(this);
11454
11455				document[fn.exitFullscreen]();
11456
11457				this.on('change', onFullScreenExit);
11458			}.bind(this));
11459		},
11460		toggle: function (elem) {
11461			return this.isFullscreen ? this.exit() : this.request(elem);
11462		},
11463		onchange: function (callback) {
11464			this.on('change', callback);
11465		},
11466		onerror: function (callback) {
11467			this.on('error', callback);
11468		},
11469		on: function (event, callback) {
11470			var eventName = eventNameMap[event];
11471			if (eventName) {
11472				document.addEventListener(eventName, callback, false);
11473			}
11474		},
11475		off: function (event, callback) {
11476			var eventName = eventNameMap[event];
11477			if (eventName) {
11478				document.removeEventListener(eventName, callback, false);
11479			}
11480		},
11481		raw: fn
11482	};
11483
11484	if (!fn) {
11485		if (isCommonjs) {
11486			module.exports = false;
11487		} else {
11488			window.ngscreenfull = false;
11489		}
11490
11491		return;
11492	}
11493
11494	Object.defineProperties(ngscreenfull, {
11495		isFullscreen: {
11496			get: function () {
11497				return Boolean(document[fn.fullscreenElement]);
11498			}
11499		},
11500		element: {
11501			enumerable: true,
11502			get: function () {
11503				return document[fn.fullscreenElement];
11504			}
11505		},
11506		enabled: {
11507			enumerable: true,
11508			get: function () {
11509				// Coerce to boolean in case of old WebKit
11510				return Boolean(document[fn.fullscreenEnabled]);
11511			}
11512		}
11513	});
11514
11515	if (isCommonjs) {
11516		module.exports = ngscreenfull;
11517	} else {
11518		window.ngscreenfull = ngscreenfull;
11519	}
11520})();
11521
11522
11523  
11524//##########################################################################################################################
11525//## Shifty ################################################################################################################
11526//##########################################################################################################################
11527  
11528 /*!
11529 * Shifty
11530 * By Jeremy Kahn - [email protected]
11531 */
11532
11533// external module EMBEDED in nanogallery
11534// NGY BUILD:
11535// 
11536// replace "Tweenable" with "NGTweenable"
11537// replace "define.amd" with "define.amdDISABLED"
11538// replace "var root = this || Function('return this')();" with "const root = typeof window !== 'undefined' ? window : global"
11539
11540/* shifty - v1.5.3 - 2016-11-29 - http://jeremyckahn.github.io/shifty */
11541;(function () {
11542  const root = typeof window !== 'undefined' ? window : global
11543
11544/**
11545 * Shifty Core
11546 * By Jeremy Kahn - [email protected]
11547 */
11548
11549var NGTweenable = (function () {
11550
11551  'use strict';
11552
11553  // Aliases that get defined later in this function
11554  var formula;
11555
11556  // CONSTANTS
11557  var DEFAULT_SCHEDULE_FUNCTION;
11558  var DEFAULT_EASING = 'linear';
11559  var DEFAULT_DURATION = 500;
11560  var UPDATE_TIME = 1000 / 60;
11561
11562  var _now = Date.now
11563       ? Date.now
11564       : function () {return +new Date();};
11565
11566  var now = typeof SHIFTY_DEBUG_NOW !== 'undefined' ? SHIFTY_DEBUG_NOW : _now;
11567
11568  if (typeof window !== 'undefined') {
11569    // requestAnimationFrame() shim by Paul Irish (modified for Shifty)
11570    // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
11571    DEFAULT_SCHEDULE_FUNCTION = window.requestAnimationFrame
11572       || window.webkitRequestAnimationFrame
11573       || window.oRequestAnimationFrame
11574       || window.msRequestAnimationFrame
11575       || (window.mozCancelRequestAnimationFrame
11576       && window.mozRequestAnimationFrame)
11577       || setTimeout;
11578  } else {
11579    DEFAULT_SCHEDULE_FUNCTION = setTimeout;
11580  }
11581
11582  function noop () {
11583    // NOOP!
11584  }
11585
11586  /**
11587   * Handy shortcut for doing a for-in loop. This is not a "normal" each
11588   * function, it is optimized for Shifty.  The iterator function only receives
11589   * the property name, not the value.
11590   * @param {Object} obj
11591   * @param {Function(string)} fn
11592   * @private
11593   */
11594  function each (obj, fn) {
11595    var key;
11596    for (key in obj) {
11597      if (Object.hasOwnProperty.call(obj, key)) {
11598        fn(key);
11599      }
11600    }
11601  }
11602
11603  /**
11604   * Perform a shallow copy of Object properties.
11605   * @param {Object} targetObject The object to copy into
11606   * @param {Object} srcObject The object to copy from
11607   * @return {Object} A reference to the augmented `targetObj` Object
11608   * @private
11609   */
11610  function shallowCopy (targetObj, srcObj) {
11611    each(srcObj, function (prop) {
11612      targetObj[prop] = srcObj[prop];
11613    });
11614
11615    return targetObj;
11616  }
11617
11618  /**
11619   * Copies each property from src onto target, but only if the property to
11620   * copy to target is undefined.
11621   * @param {Object} target Missing properties in this Object are filled in
11622   * @param {Object} src
11623   * @private
11624   */
11625  function defaults (target, src) {
11626    each(src, function (prop) {
11627      if (typeof target[prop] === 'undefined') {
11628        target[prop] = src[prop];
11629      }
11630    });
11631  }
11632
11633  /**
11634   * Calculates the interpolated tween values of an Object for a given
11635   * timestamp.
11636   * @param {Number} forPosition The position to compute the state for.
11637   * @param {Object} currentState Current state properties.
11638   * @param {Object} originalState: The original state properties the Object is
11639   * tweening from.
11640   * @param {Object} targetState: The destination state properties the Object
11641   * is tweening to.
11642   * @param {number} duration: The length of the tween in milliseconds.
11643   * @param {number} timestamp: The UNIX epoch time at which the tween began.
11644   * @param {Object} easing: This Object's keys must correspond to the keys in
11645   * targetState.
11646   * @private
11647   */
11648  function tweenProps (forPosition, currentState, originalState, targetState,
11649    duration, timestamp, easing) {
11650    var normalizedPosition =
11651        forPosition < timestamp ? 0 : (forPosition - timestamp) / duration;
11652
11653
11654    var prop;
11655    var easingObjectProp;
11656    var easingFn;
11657    for (prop in currentState) {
11658      if (currentState.hasOwnProperty(prop)) {
11659        easingObjectProp = easing[prop];
11660        easingFn = typeof easingObjectProp === 'function'
11661          ? easingObjectProp
11662          : formula[easingObjectProp];
11663
11664        currentState[prop] = tweenProp(
11665          originalState[prop],
11666          targetState[prop],
11667          easingFn,
11668          normalizedPosition
11669        );
11670      }
11671    }
11672
11673    return currentState;
11674  }
11675
11676  /**
11677   * Tweens a single property.
11678   * @param {number} start The value that the tween started from.
11679   * @param {number} end The value that the tween should end at.
11680   * @param {Function} easingFunc The easing curve to apply to the tween.
11681   * @param {number} position The normalized position (between 0.0 and 1.0) to
11682   * calculate the midpoint of 'start' and 'end' against.
11683   * @return {number} The tweened value.
11684   * @private
11685   */
11686  function tweenProp (start, end, easingFunc, position) {
11687    return start + (end - start) * easingFunc(position);
11688  }
11689
11690  /**
11691   * Applies a filter to NGTweenable instance.
11692   * @param {NGTweenable} tweenable The `NGTweenable` instance to call the filter
11693   * upon.
11694   * @param {String} filterName The name of the filter to apply.
11695   * @private
11696   */
11697  function applyFilter (tweenable, filterName) {
11698    var filters = NGTweenable.prototype.filter;
11699    var args = tweenable._filterArgs;
11700
11701    each(filters, function (name) {
11702      if (typeof filters[name][filterName] !== 'undefined') {
11703        filters[name][filterName].apply(tweenable, args);
11704      }
11705    });
11706  }
11707
11708  var timeoutHandler_endTime;
11709  var timeoutHandler_currentTime;
11710  var timeoutHandler_isEnded;
11711  var timeoutHandler_offset;
11712  /**
11713   * Handles the update logic for one step of a tween.
11714   * @param {NGTweenable} tweenable
11715   * @param {number} timestamp
11716   * @param {number} delay
11717   * @param {number} duration
11718   * @param {Object} currentState
11719   * @param {Object} originalState
11720   * @param {Object} targetState
11721   * @param {Object} easing
11722   * @param {Function(Object, *, number)} step
11723   * @param {Function(Function,number)}} schedule
11724   * @param {number=} opt_currentTimeOverride Needed for accurate timestamp in
11725   * NGTweenable#seek.
11726   * @private
11727   */
11728  function timeoutHandler (tweenable, timestamp, delay, duration, currentState,
11729    originalState, targetState, easing, step, schedule,
11730    opt_currentTimeOverride) {
11731
11732    timeoutHandler_endTime = timestamp + delay + duration;
11733
11734    timeoutHandler_currentTime =
11735    Math.min(opt_currentTimeOverride || now(), timeoutHandler_endTime);
11736
11737    timeoutHandler_isEnded =
11738      timeoutHandler_currentTime >= timeoutHandler_endTime;
11739
11740    timeoutHandler_offset = duration - (
11741      timeoutHandler_endTime - timeoutHandler_currentTime);
11742
11743    if (tweenable.isPlaying()) {
11744      if (timeoutHandler_isEnded) {
11745        step(targetState, tweenable._attachment, timeoutHandler_offset);
11746        tweenable.stop(true);
11747      } else {
11748        tweenable._scheduleId =
11749          schedule(tweenable._timeoutHandler, UPDATE_TIME);
11750
11751        applyFilter(tweenable, 'beforeTween');
11752
11753        // If the animation has not yet reached the start point (e.g., there was
11754        // delay that has not yet completed), just interpolate the starting
11755        // position of the tween.
11756        if (timeoutHandler_currentTime < (timestamp + delay)) {
11757          tweenProps(1, currentState, originalState, targetState, 1, 1, easing);
11758        } else {
11759          tweenProps(timeoutHandler_currentTime, currentState, originalState,
11760            targetState, duration, timestamp + delay, easing);
11761        }
11762
11763        applyFilter(tweenable, 'afterTween');
11764
11765        step(currentState, tweenable._attachment, timeoutHandler_offset);
11766      }
11767    }
11768  }
11769
11770
11771  /**
11772   * Creates a usable easing Object from a string, a function or another easing
11773   * Object.  If `easing` is an Object, then this function clones it and fills
11774   * in the missing properties with `"linear"`.
11775   * @param {Object.<string|Function>} fromTweenParams
11776   * @param {Object|string|Function} easing
11777   * @return {Object.<string|Function>}
11778   * @private
11779   */
11780  function composeEasingObject (fromTweenParams, easing) {
11781    var composedEasing = {};
11782    var typeofEasing = typeof easing;
11783
11784    if (typeofEasing === 'string' || typeofEasing === 'function') {
11785      each(fromTweenParams, function (prop) {
11786        composedEasing[prop] = easing;
11787      });
11788    } else {
11789      each(fromTweenParams, function (prop) {
11790        if (!composedEasing[prop]) {
11791          composedEasing[prop] = easing[prop] || DEFAULT_EASING;
11792        }
11793      });
11794    }
11795
11796    return composedEasing;
11797  }
11798
11799  /**
11800   * NGTweenable constructor.
11801   * @class NGTweenable
11802   * @param {Object=} opt_initialState The values that the initial tween should
11803   * start at if a `from` object is not provided to `{{#crossLink
11804   * "NGTweenable/tween:method"}}{{/crossLink}}` or `{{#crossLink
11805   * "NGTweenable/setConfig:method"}}{{/crossLink}}`.
11806   * @param {Object=} opt_config Configuration object to be passed to
11807   * `{{#crossLink "NGTweenable/setConfig:method"}}{{/crossLink}}`.
11808   * @module NGTweenable
11809   * @constructor
11810   */
11811  function NGTweenable (opt_initialState, opt_config) {
11812    this._currentState = opt_initialState || {};
11813    this._configured = false;
11814    this._scheduleFunction = DEFAULT_SCHEDULE_FUNCTION;
11815
11816    // To prevent unnecessary calls to setConfig do not set default
11817    // configuration here.  Only set default configuration immediately before
11818    // tweening if none has been set.
11819    if (typeof opt_config !== 'undefined') {
11820      this.setConfig(opt_config);
11821    }
11822  }
11823
11824  /**
11825   * Configure and start a tween.
11826   * @method tween
11827   * @param {Object=} opt_config Configuration object to be passed to
11828   * `{{#crossLink "NGTweenable/setConfig:method"}}{{/crossLink}}`.
11829   * @chainable
11830   */
11831  NGTweenable.prototype.tween = function (opt_config) {
11832    if (this._isTweening) {
11833      return this;
11834    }
11835
11836    // Only set default config if no configuration has been set previously and
11837    // none is provided now.
11838    if (opt_config !== undefined || !this._configured) {
11839      this.setConfig(opt_config);
11840    }
11841
11842    this._timestamp = now();
11843    this._start(this.get(), this._attachment);
11844    return this.resume();
11845  };
11846
11847  /**
11848   * Configure a tween that will start at some point in the future.
11849   *
11850   * @method setConfig
11851   * @param {Object} config The following values are valid:
11852   * - __from__ (_Object=_): Starting position.  If omitted, `{{#crossLink
11853   *   "NGTweenable/get:method"}}get(){{/crossLink}}` is used.
11854   * - __to__ (_Object=_): Ending position.
11855   * - __duration__ (_number=_): How many milliseconds to animate for.
11856   * - __delay__ (_delay=_): How many milliseconds to wait before starting the
11857   *   tween.
11858   * - __start__ (_Function(Object, *)_): Function to execute when the tween
11859   *   begins.  Receives the state of the tween as the first parameter and
11860   *   `attachment` as the second parameter.
11861   * - __step__ (_Function(Object, *, number)_): Function to execute on every
11862   *   tick.  Receives `{{#crossLink
11863   *   "NGTweenable/get:method"}}get(){{/crossLink}}` as the first parameter,
11864   *   `attachment` as the second parameter, and the time elapsed since the
11865   *   start of the tween as the third. This function is not called on the
11866   *   final step of the animation, but `finish` is.
11867   * - __finish__ (_Function(Object, *)_): Function to execute upon tween
11868   *   completion.  Receives the state of the tween as the first parameter and
11869   *   `attachment` as the second parameter.
11870   * - __easing__ (_Object.<string|Function>|string|Function=_): Easing curve
11871   *   name(s) or function(s) to use for the tween.
11872   * - __attachment__ (_*_): Cached value that is passed to the
11873   *   `step`/`start`/`finish` methods.
11874   * @chainable
11875   */
11876  NGTweenable.prototype.setConfig = function (config) {
11877    config = config || {};
11878    this._configured = true;
11879
11880    // Attach something to this NGTweenable instance (e.g.: a DOM element, an
11881    // object, a string, etc.);
11882    this._attachment = config.attachment;
11883
11884    // Init the internal state
11885    this._pausedAtTime = null;
11886    this._scheduleId = null;
11887    this._delay = config.delay || 0;
11888    this._start = config.start || noop;
11889    this._step = config.step || noop;
11890    this._finish = config.finish || noop;
11891    this._duration = config.duration || DEFAULT_DURATION;
11892    this._currentState = shallowCopy({}, config.from || this.get());
11893    this._originalState = this.get();
11894    this._targetState = shallowCopy({}, config.to || this.get());
11895
11896    var self = this;
11897    this._timeoutHandler = function () {
11898      timeoutHandler(self,
11899        self._timestamp,
11900        self._delay,
11901        self._duration,
11902        self._currentState,
11903        self._originalState,
11904        self._targetState,
11905        self._easing,
11906        self._step,
11907        self._scheduleFunction
11908      );
11909    };
11910
11911    // Aliases used below
11912    var currentState = this._currentState;
11913    var targetState = this._targetState;
11914
11915    // Ensure that there is always something to tween to.
11916    defaults(targetState, currentState);
11917
11918    this._easing = composeEasingObject(
11919      currentState, config.easing || DEFAULT_EASING);
11920
11921    this._filterArgs =
11922      [currentState, this._originalState, targetState, this._easing];
11923
11924    applyFilter(this, 'tweenCreated');
11925    return this;
11926  };
11927
11928  /**
11929   * @method get
11930   * @return {Object} The current state.
11931   */
11932  NGTweenable.prototype.get = function () {
11933    return shallowCopy({}, this._currentState);
11934  };
11935
11936  /**
11937   * @method set
11938   * @param {Object} state The current state.
11939   */
11940  NGTweenable.prototype.set = function (state) {
11941    this._currentState = state;
11942  };
11943
11944  /**
11945   * Pause a tween.  Paused tweens can be resumed from the point at which they
11946   * were paused.  This is different from `{{#crossLink
11947   * "NGTweenable/stop:method"}}{{/crossLink}}`, as that method
11948   * causes a tween to start over when it is resumed.
11949   * @method pause
11950   * @chainable
11951   */
11952  NGTweenable.prototype.pause = function () {
11953    this._pausedAtTime = now();
11954    this._isPaused = true;
11955    return this;
11956  };
11957
11958  /**
11959   * Resume a paused tween.
11960   * @method resume
11961   * @chainable
11962   */
11963  NGTweenable.prototype.resume = function () {
11964    if (this._isPaused) {
11965      this._timestamp += now() - this._pausedAtTime;
11966    }
11967
11968    this._isPaused = false;
11969    this._isTweening = true;
11970
11971    this._timeoutHandler();
11972
11973    return this;
11974  };
11975
11976  /**
11977   * Move the state of the animation to a specific point in the tween's
11978   * timeline.  If the animation is not running, this will cause the `step`
11979   * handlers to be called.
11980   * @method seek
11981   * @param {millisecond} millisecond The millisecond of the animation to seek
11982   * to.  This must not be less than `0`.
11983   * @chainable
11984   */
11985  NGTweenable.prototype.seek = function (millisecond) {
11986    millisecond = Math.max(millisecond, 0);
11987    var currentTime = now();
11988
11989    if ((this._timestamp + millisecond) === 0) {
11990      return this;
11991    }
11992
11993    this._timestamp = currentTime - millisecond;
11994
11995    if (!this.isPlaying()) {
11996      this._isTweening = true;
11997      this._isPaused = false;
11998
11999      // If the animation is not running, call timeoutHandler to make sure that
12000      // any step handlers are run.
12001      timeoutHandler(this,
12002        this._timestamp,
12003        this._delay,
12004        this._duration,
12005        this._currentState,
12006        this._originalState,
12007        this._targetState,
12008        this._easing,
12009        this._step,
12010        this._scheduleFunction,
12011        currentTime
12012      );
12013
12014      this.pause();
12015    }
12016
12017    return this;
12018  };
12019
12020  /**
12021   * Stops and cancels a tween.
12022   * @param {boolean=} gotoEnd If `false` or omitted, the tween just stops at
12023   * its current state, and the `finish` handler is not invoked.  If `true`,
12024   * the tweened object's values are instantly set to the target values, and
12025   * `finish` is invoked.
12026   * @method stop
12027   * @chainable
12028   */
12029  NGTweenable.prototype.stop = function (gotoEnd) {
12030    this._isTweening = false;
12031    this._isPaused = false;
12032    this._timeoutHandler = noop;
12033
12034    (root.cancelAnimationFrame            ||
12035    root.webkitCancelAnimationFrame     ||
12036    root.oCancelAnimationFrame          ||
12037    root.msCancelAnimationFrame         ||
12038    root.mozCancelRequestAnimationFrame ||
12039    root.clearTimeout)(this._scheduleId);
12040
12041    if (gotoEnd) {
12042      applyFilter(this, 'beforeTween');
12043      tweenProps(
12044        1,
12045        this._currentState,
12046        this._originalState,
12047        this._targetState,
12048        1,
12049        0,
12050        this._easing
12051      );
12052      applyFilter(this, 'afterTween');
12053      applyFilter(this, 'afterTweenEnd');
12054      this._finish.call(this, this._currentState, this._attachment);
12055    }
12056
12057    return this;
12058  };
12059
12060  /**
12061   * @method isPlaying
12062   * @return {boolean} Whether or not a tween is running.
12063   */
12064  NGTweenable.prototype.isPlaying = function () {
12065    return this._isTweening && !this._isPaused;
12066  };
12067
12068  /**
12069   * Set a custom schedule function.
12070   *
12071   * If a custom function is not set,
12072   * [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame)
12073   * is used if available, otherwise
12074   * [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/Window.setTimeout)
12075   * is used.
12076   * @method setScheduleFunction
12077   * @param {Function(Function,number)} scheduleFunction The function to be
12078   * used to schedule the next frame to be rendered.
12079   */
12080  NGTweenable.prototype.setScheduleFunction = function (scheduleFunction) {
12081    this._scheduleFunction = scheduleFunction;
12082  };
12083
12084  /**
12085   * `delete` all "own" properties.  Call this when the `NGTweenable` instance
12086   * is no longer needed to free memory.
12087   * @method dispose
12088   */
12089  NGTweenable.prototype.dispose = function () {
12090    var prop;
12091    for (prop in this) {
12092      if (this.hasOwnProperty(prop)) {
12093        delete this[prop];
12094      }
12095    }
12096  };
12097
12098  /**
12099   * Filters are used for transforming the properties of a tween at various
12100   * points in a NGTweenable's life cycle.  See the README for more info on this.
12101   * @private
12102   */
12103  NGTweenable.prototype.filter = {};
12104
12105  /**
12106   * This object contains all of the tweens available to Shifty.  It is
12107   * extensible - simply attach properties to the `NGTweenable.prototype.formula`
12108   * Object following the same format as `linear`.
12109   *
12110   * `pos` should be a normalized `number` (between 0 and 1).
12111   * @property formula
12112   * @type {Object(function)}
12113   */
12114  NGTweenable.prototype.formula = {
12115    linear: function (pos) {
12116      return pos;
12117    }
12118  };
12119
12120  formula = NGTweenable.prototype.formula;
12121
12122  shallowCopy(NGTweenable, {
12123    'now': now
12124    ,'each': each
12125    ,'tweenProps': tweenProps
12126    ,'tweenProp': tweenProp
12127    ,'applyFilter': applyFilter
12128    ,'shallowCopy': shallowCopy
12129    ,'defaults': defaults
12130    ,'composeEasingObject': composeEasingObject
12131  });
12132
12133  // `root` is provided in the intro/outro files.
12134
12135  // A hook used for unit testing.
12136  if (typeof SHIFTY_DEBUG_NOW === 'function') {
12137    root.timeoutHandler = timeoutHandler;
12138  }
12139
12140  // Bootstrap NGTweenable appropriately for the environment.
12141  if (typeof exports === 'object') {
12142    // CommonJS
12143    module.exports = NGTweenable;
12144  } else if (typeof define === 'function' && define.amdDISABLED) {
12145    // AMD
12146    define(function () {return NGTweenable;});
12147  } else if (typeof root.NGTweenable === 'undefined') {
12148    // Browser: Make `NGTweenable` globally accessible.
12149    root.NGTweenable = NGTweenable;
12150  }
12151
12152  return NGTweenable;
12153
12154} ());
12155
12156/*!
12157 * All equations are adapted from Thomas Fuchs'
12158 * [Scripty2](https://github.com/madrobby/scripty2/blob/master/src/effects/transitions/penner.js).
12159 *
12160 * Based on Easing Equations (c) 2003 [Robert
12161 * Penner](http://www.robertpenner.com/), all rights reserved. This work is
12162 * [subject to terms](http://www.robertpenner.com/easing_terms_of_use.html).
12163 */
12164
12165/*!
12166 *  TERMS OF USE - EASING EQUATIONS
12167 *  Open source under the BSD License.
12168 *  Easing Equations (c) 2003 Robert Penner, all rights reserved.
12169 */
12170
12171;(function () {
12172
12173  NGTweenable.shallowCopy(NGTweenable.prototype.formula, {
12174    easeInQuad: function (pos) {
12175      return Math.pow(pos, 2);
12176    },
12177
12178    easeOutQuad: function (pos) {
12179      return -(Math.pow((pos - 1), 2) - 1);
12180    },
12181
12182    easeInOutQuad: function (pos) {
12183      if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(pos,2);}
12184      return -0.5 * ((pos -= 2) * pos - 2);
12185    },
12186
12187    easeInCubic: function (pos) {
12188      return Math.pow(pos, 3);
12189    },
12190
12191    easeOutCubic: function (pos) {
12192      return (Math.pow((pos - 1), 3) + 1);
12193    },
12194
12195    easeInOutCubic: function (pos) {
12196      if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(pos,3);}
12197      return 0.5 * (Math.pow((pos - 2),3) + 2);
12198    },
12199
12200    easeInQuart: function (pos) {
12201      return Math.pow(pos, 4);
12202    },
12203
12204    easeOutQuart: function (pos) {
12205      return -(Math.pow((pos - 1), 4) - 1);
12206    },
12207
12208    easeInOutQuart: function (pos) {
12209      if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(pos,4);}
12210      return -0.5 * ((pos -= 2) * Math.pow(pos,3) - 2);
12211    },
12212
12213    easeInQuint: function (pos) {
12214      return Math.pow(pos, 5);
12215    },
12216
12217    easeOutQuint: function (pos) {
12218      return (Math.pow((pos - 1), 5) + 1);
12219    },
12220
12221    easeInOutQuint: function (pos) {
12222      if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(pos,5);}
12223      return 0.5 * (Math.pow((pos - 2),5) + 2);
12224    },
12225
12226    easeInSine: function (pos) {
12227      return -Math.cos(pos * (Math.PI / 2)) + 1;
12228    },
12229
12230    easeOutSine: function (pos) {
12231      return Math.sin(pos * (Math.PI / 2));
12232    },
12233
12234    easeInOutSine: function (pos) {
12235      return (-0.5 * (Math.cos(Math.PI * pos) - 1));
12236    },
12237
12238    easeInExpo: function (pos) {
12239      return (pos === 0) ? 0 : Math.pow(2, 10 * (pos - 1));
12240    },
12241
12242    easeOutExpo: function (pos) {
12243      return (pos === 1) ? 1 : -Math.pow(2, -10 * pos) + 1;
12244    },
12245
12246    easeInOutExpo: function (pos) {
12247      if (pos === 0) {return 0;}
12248      if (pos === 1) {return 1;}
12249      if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(2,10 * (pos - 1));}
12250      return 0.5 * (-Math.pow(2, -10 * --pos) + 2);
12251    },
12252
12253    easeInCirc: function (pos) {
12254      return -(Math.sqrt(1 - (pos * pos)) - 1);
12255    },
12256
12257    easeOutCirc: function (pos) {
12258      return Math.sqrt(1 - Math.pow((pos - 1), 2));
12259    },
12260
12261    easeInOutCirc: function (pos) {
12262      if ((pos /= 0.5) < 1) {return -0.5 * (Math.sqrt(1 - pos * pos) - 1);}
12263      return 0.5 * (Math.sqrt(1 - (pos -= 2) * pos) + 1);
12264    },
12265
12266    easeOutBounce: function (pos) {
12267      if ((pos) < (1 / 2.75)) {
12268        return (7.5625 * pos * pos);
12269      } else if (pos < (2 / 2.75)) {
12270        return (7.5625 * (pos -= (1.5 / 2.75)) * pos + 0.75);
12271      } else if (pos < (2.5 / 2.75)) {
12272        return (7.5625 * (pos -= (2.25 / 2.75)) * pos + 0.9375);
12273      } else {
12274        return (7.5625 * (pos -= (2.625 / 2.75)) * pos + 0.984375);
12275      }
12276    },
12277
12278    easeInBack: function (pos) {
12279      var s = 1.70158;
12280      return (pos) * pos * ((s + 1) * pos - s);
12281    },
12282
12283    easeOutBack: function (pos) {
12284      var s = 1.70158;
12285      return (pos = pos - 1) * pos * ((s + 1) * pos + s) + 1;
12286    },
12287
12288    easeInOutBack: function (pos) {
12289      var s = 1.70158;
12290      if ((pos /= 0.5) < 1) {
12291        return 0.5 * (pos * pos * (((s *= (1.525)) + 1) * pos - s));
12292      }
12293      return 0.5 * ((pos -= 2) * pos * (((s *= (1.525)) + 1) * pos + s) + 2);
12294    },
12295
12296    elastic: function (pos) {
12297      // jshint maxlen:90
12298      return -1 * Math.pow(4,-8 * pos) * Math.sin((pos * 6 - 1) * (2 * Math.PI) / 2) + 1;
12299    },
12300
12301    swingFromTo: function (pos) {
12302      var s = 1.70158;
12303      return ((pos /= 0.5) < 1) ?
12304          0.5 * (pos * pos * (((s *= (1.525)) + 1) * pos - s)) :
12305          0.5 * ((pos -= 2) * pos * (((s *= (1.525)) + 1) * pos + s) + 2);
12306    },
12307
12308    swingFrom: function (pos) {
12309      var s = 1.70158;
12310      return pos * pos * ((s + 1) * pos - s);
12311    },
12312
12313    swingTo: function (pos) {
12314      var s = 1.70158;
12315      return (pos -= 1) * pos * ((s + 1) * pos + s) + 1;
12316    },
12317
12318    bounce: function (pos) {
12319      if (pos < (1 / 2.75)) {
12320        return (7.5625 * pos * pos);
12321      } else if (pos < (2 / 2.75)) {
12322        return (7.5625 * (pos -= (1.5 / 2.75)) * pos + 0.75);
12323      } else if (pos < (2.5 / 2.75)) {
12324        return (7.5625 * (pos -= (2.25 / 2.75)) * pos + 0.9375);
12325      } else {
12326        return (7.5625 * (pos -= (2.625 / 2.75)) * pos + 0.984375);
12327      }
12328    },
12329
12330    bouncePast: function (pos) {
12331      if (pos < (1 / 2.75)) {
12332        return (7.5625 * pos * pos);
12333      } else if (pos < (2 / 2.75)) {
12334        return 2 - (7.5625 * (pos -= (1.5 / 2.75)) * pos + 0.75);
12335      } else if (pos < (2.5 / 2.75)) {
12336        return 2 - (7.5625 * (pos -= (2.25 / 2.75)) * pos + 0.9375);
12337      } else {
12338        return 2 - (7.5625 * (pos -= (2.625 / 2.75)) * pos + 0.984375);
12339      }
12340    },
12341
12342    easeFromTo: function (pos) {
12343      if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(pos,4);}
12344      return -0.5 * ((pos -= 2) * Math.pow(pos,3) - 2);
12345    },
12346
12347    easeFrom: function (pos) {
12348      return Math.pow(pos,4);
12349    },
12350
12351    easeTo: function (pos) {
12352      return Math.pow(pos,0.25);
12353    }
12354  });
12355
12356}());
12357
12358// jshint maxlen:100
12359/**
12360 * The Bezier magic in this file is adapted/copied almost wholesale from
12361 * [Scripty2](https://github.com/madrobby/scripty2/blob/master/src/effects/transitions/cubic-bezier.js),
12362 * which was adapted from Apple code (which probably came from
12363 * [here](http://opensource.apple.com/source/WebCore/WebCore-955.66/platform/graphics/UnitBezier.h)).
12364 * Special thanks to Apple and Thomas Fuchs for much of this code.
12365 */
12366
12367/**
12368 *  Copyright (c) 2006 Apple Computer, Inc. All rights reserved.
12369 *
12370 *  Redistribution and use in source and binary forms, with or without
12371 *  modification, are pfervereted provided that the following conditions are met:
12372 *
12373 *  1. Redistributions of source code must retain the above copyright notice,
12374 *  this list of conditions and the following disclaimer.
12375 *
12376 *  2. Redistributions in binary form must reproduce the above copyright notice,
12377 *  this list of conditions and the following disclaimer in the documentation
12378 *  and/or other materials provided with the distribution.
12379 *
12380 *  3. Neither the name of the copyright holder(s) nor the names of any
12381 *  contributors may be used to endorse or promote products derived from
12382 *  this software without specific prior written permission.
12383 *
12384 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
12385 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
12386 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
12387 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
12388 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
12389 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
12390 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
12391 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
12392 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
12393 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
12394 *  POSSIBILITY OF SUCH DAMAGE.
12395 */
12396;(function () {
12397  // port of webkit cubic bezier handling by http://www.netzgesta.de/dev/
12398  function cubicBezierAtTime(t,p1x,p1y,p2x,p2y,duration) {
12399    var ax = 0,bx = 0,cx = 0,ay = 0,by = 0,cy = 0;
12400    function sampleCurveX(t) {
12401      return ((ax * t + bx) * t + cx) * t;
12402    }
12403    function sampleCurveY(t) {
12404      return ((ay * t + by) * t + cy) * t;
12405    }
12406    function sampleCurveDerivativeX(t) {
12407      return (3.0 * ax * t + 2.0 * bx) * t + cx;
12408    }
12409    function solveEpsilon(duration) {
12410      return 1.0 / (200.0 * duration);
12411    }
12412    function solve(x,epsilon) {
12413      return sampleCurveY(solveCurveX(x, epsilon));
12414    }
12415    function fabs(n) {
12416      if (n >= 0) {
12417        return n;
12418      } else {
12419        return 0 - n;
12420      }
12421    }
12422    function solveCurveX(x, epsilon) {
12423      var t0,t1,t2,x2,d2,i;
12424      for (t2 = x, i = 0; i < 8; i++) {
12425        x2 = sampleCurveX(t2) - x;
12426        if (fabs(x2) < epsilon) {
12427          return t2;
12428        }
12429        d2 = sampleCurveDerivativeX(t2);
12430        if (fabs(d2) < 1e-6) {
12431          break;
12432        }
12433        t2 = t2 - x2 / d2;
12434      }
12435      t0 = 0.0;
12436      t1 = 1.0;
12437      t2 = x;
12438      if (t2 < t0) {
12439        return t0;
12440      }
12441      if (t2 > t1) {
12442        return t1;
12443      }
12444      while (t0 < t1) {
12445        x2 = sampleCurveX(t2);
12446        if (fabs(x2 - x) < epsilon) {
12447          return t2;
12448        }
12449        if (x > x2) {
12450          t0 = t2;
12451        }else {
12452          t1 = t2;
12453        }
12454        t2 = (t1 - t0) * 0.5 + t0;
12455      }
12456      return t2; // Failure.
12457    }
12458    cx = 3.0 * p1x;
12459    bx = 3.0 * (p2x - p1x) - cx;
12460    ax = 1.0 - cx - bx;
12461    cy = 3.0 * p1y;
12462    by = 3.0 * (p2y - p1y) - cy;
12463    ay = 1.0 - cy - by;
12464    return solve(t, solveEpsilon(duration));
12465  }
12466  /**
12467   *  getCubicBezierTransition(x1, y1, x2, y2) -> Function
12468   *
12469   *  Generates a transition easing function that is compatible
12470   *  with WebKit's CSS transitions `-webkit-transition-timing-function`
12471   *  CSS property.
12472   *
12473   *  The W3C has more information about CSS3 transition timing functions:
12474   *  http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag
12475   *
12476   *  @param {number} x1
12477   *  @param {number} y1
12478   *  @param {number} x2
12479   *  @param {number} y2
12480   *  @return {function}
12481   *  @private
12482   */
12483  function getCubicBezierTransition (x1, y1, x2, y2) {
12484    return function (pos) {
12485      return cubicBezierAtTime(pos,x1,y1,x2,y2,1);
12486    };
12487  }
12488  // End ported code
12489
12490  /**
12491   * Create a Bezier easing function and attach it to `{{#crossLink
12492   * "NGTweenable/formula:property"}}NGTweenable#formula{{/crossLink}}`.  This
12493   * function gives you total control over the easing curve.  Matthew Lein's
12494   * [Ceaser](http://matthewlein.com/ceaser/) is a useful tool for visualizing
12495   * the curves you can make with this function.
12496   * @method setBezierFunction
12497   * @param {string} name The name of the easing curve.  Overwrites the old
12498   * easing function on `{{#crossLink
12499   * "NGTweenable/formula:property"}}NGTweenable#formula{{/crossLink}}` if it
12500   * exists.
12501   * @param {number} x1
12502   * @param {number} y1
12503   * @param {number} x2
12504   * @param {number} y2
12505   * @return {function} The easing function that was attached to
12506   * NGTweenable.prototype.formula.
12507   */
12508  NGTweenable.setBezierFunction = function (name, x1, y1, x2, y2) {
12509    var cubicBezierTransition = getCubicBezierTransition(x1, y1, x2, y2);
12510    cubicBezierTransition.displayName = name;
12511    cubicBezierTransition.x1 = x1;
12512    cubicBezierTransition.y1 = y1;
12513    cubicBezierTransition.x2 = x2;
12514    cubicBezierTransition.y2 = y2;
12515
12516    return NGTweenable.prototype.formula[name] = cubicBezierTransition;
12517  };
12518
12519
12520  /**
12521   * `delete` an easing function from `{{#crossLink
12522   * "NGTweenable/formula:property"}}NGTweenable#formula{{/crossLink}}`.  Be
12523   * careful with this method, as it `delete`s whatever easing formula matches
12524   * `name` (which means you can delete standard Shifty easing functions).
12525   * @method unsetBezierFunction
12526   * @param {string} name The name of the easing function to delete.
12527   * @return {function}
12528   */
12529  NGTweenable.unsetBezierFunction = function (name) {
12530    delete NGTweenable.prototype.formula[name];
12531  };
12532
12533})();
12534
12535;(function () {
12536
12537  function getInterpolatedValues (
12538    from, current, targetState, position, easing, delay) {
12539    return NGTweenable.tweenProps(
12540      position, current, from, targetState, 1, delay, easing);
12541  }
12542
12543  // Fake a NGTweenable and patch some internals.  This approach allows us to
12544  // skip uneccessary processing and object recreation, cutting down on garbage
12545  // collection pauses.
12546  var mockNGTweenable = new NGTweenable();
12547  mockNGTweenable._filterArgs = [];
12548
12549  /**
12550   * Compute the midpoint of two Objects.  This method effectively calculates a
12551   * specific frame of animation that `{{#crossLink
12552   * "NGTweenable/tween:method"}}{{/crossLink}}` does many times over the course
12553   * of a full tween.
12554   *
12555   *     var interpolatedValues = NGTweenable.interpolate({
12556   *       width: '100px',
12557   *       opacity: 0,
12558   *       color: '#fff'
12559   *     }, {
12560   *       width: '200px',
12561   *       opacity: 1,
12562   *       color: '#000'
12563   *     }, 0.5);
12564   *
12565   *     console.log(interpolatedValues);
12566   *     // {opacity: 0.5, width: "150px", color: "rgb(127,127,127)"}
12567   *
12568   * @static
12569   * @method interpolate
12570   * @param {Object} from The starting values to tween from.
12571   * @param {Object} targetState The ending values to tween to.
12572   * @param {number} position The normalized position value (between `0.0` and
12573   * `1.0`) to interpolate the values between `from` and `to` for.  `from`
12574   * represents `0` and `to` represents `1`.
12575   * @param {Object.<string|Function>|string|Function} easing The easing
12576   * curve(s) to calculate the midpoint against.  You can reference any easing
12577   * function attached to `NGTweenable.prototype.formula`, or provide the easing
12578   * function(s) directly.  If omitted, this defaults to "linear".
12579   * @param {number=} opt_delay Optional delay to pad the beginning of the
12580   * interpolated tween with.  This increases the range of `position` from (`0`
12581   * through `1`) to (`0` through `1 + opt_delay`).  So, a delay of `0.5` would
12582   * increase all valid values of `position` to numbers between `0` and `1.5`.
12583   * @return {Object}
12584   */
12585  NGTweenable.interpolate = function (
12586    from, targetState, position, easing, opt_delay) {
12587
12588    var current = NGTweenable.shallowCopy({}, from);
12589    var delay = opt_delay || 0;
12590    var easingObject = NGTweenable.composeEasingObject(
12591      from, easing || 'linear');
12592
12593    mockNGTweenable.set({});
12594
12595    // Alias and reuse the _filterArgs array instead of recreating it.
12596    var filterArgs = mockNGTweenable._filterArgs;
12597    filterArgs.length = 0;
12598    filterArgs[0] = current;
12599    filterArgs[1] = from;
12600    filterArgs[2] = targetState;
12601    filterArgs[3] = easingObject;
12602
12603    // Any defined value transformation must be applied
12604    NGTweenable.applyFilter(mockNGTweenable, 'tweenCreated');
12605    NGTweenable.applyFilter(mockNGTweenable, 'beforeTween');
12606
12607    var interpolatedValues = getInterpolatedValues(
12608      from, current, targetState, position, easingObject, delay);
12609
12610    // Transform values back into their original format
12611    NGTweenable.applyFilter(mockNGTweenable, 'afterTween');
12612
12613    return interpolatedValues;
12614  };
12615
12616}());
12617
12618/**
12619 * This module adds string interpolation support to Shifty.
12620 *
12621 * The Token extension allows Shifty to tween numbers inside of strings.  Among
12622 * other things, this allows you to animate CSS properties.  For example, you
12623 * can do this:
12624 *
12625 *     var tweenable = new NGTweenable();
12626 *     tweenable.tween({
12627 *       from: { transform: 'translateX(45px)' },
12628 *       to: { transform: 'translateX(90xp)' }
12629 *     });
12630 *
12631 * `translateX(45)` will be tweened to `translateX(90)`.  To demonstrate:
12632 *
12633 *     var tweenable = new NGTweenable();
12634 *     tweenable.tween({
12635 *       from: { transform: 'translateX(45px)' },
12636 *       to: { transform: 'translateX(90px)' },
12637 *       step: function (state) {
12638 *         console.log(state.transform);
12639 *       }
12640 *     });
12641 *
12642 * The above snippet will log something like this in the console:
12643 *
12644 *     translateX(60.3px)
12645 *     ...
12646 *     translateX(76.05px)
12647 *     ...
12648 *     translateX(90px)
12649 *
12650 * Another use for this is animating colors:
12651 *
12652 *     var tweenable = new NGTweenable();
12653 *     tweenable.tween({
12654 *       from: { color: 'rgb(0,255,0)' },
12655 *       to: { color: 'rgb(255,0,255)' },
12656 *       step: function (state) {
12657 *         console.log(state.color);
12658 *       }
12659 *     });
12660 *
12661 * The above snippet will log something like this:
12662 *
12663 *     rgb(84,170,84)
12664 *     ...
12665 *     rgb(170,84,170)
12666 *     ...
12667 *     rgb(255,0,255)
12668 *
12669 * This extension also supports hexadecimal colors, in both long (`#ff00ff`)
12670 * and short (`#f0f`) forms.  Be aware that hexadecimal input values will be
12671 * converted into the equivalent RGB output values.  This is done to optimize
12672 * for performance.
12673 *
12674 *     var tweenable = new NGTweenable();
12675 *     tweenable.tween({
12676 *       from: { color: '#0f0' },
12677 *       to: { color: '#f0f' },
12678 *       step: function (state) {
12679 *         console.log(state.color);
12680 *       }
12681 *     });
12682 *
12683 * This snippet will generate the same output as the one before it because
12684 * equivalent values were supplied (just in hexadecimal form rather than RGB):
12685 *
12686 *     rgb(84,170,84)
12687 *     ...
12688 *     rgb(170,84,170)
12689 *     ...
12690 *     rgb(255,0,255)
12691 *
12692 * ## Easing support
12693 *
12694 * Easing works somewhat differently in the Token extension.  This is because
12695 * some CSS properties have multiple values in them, and you might need to
12696 * tween each value along its own easing curve.  A basic example:
12697 *
12698 *     var tweenable = new NGTweenable();
12699 *     tweenable.tween({
12700 *       from: { transform: 'translateX(0px) translateY(0px)' },
12701 *       to: { transform:   'translateX(100px) translateY(100px)' },
12702 *       easing: { transform: 'easeInQuad' },
12703 *       step: function (state) {
12704 *         console.log(state.transform);
12705 *       }
12706 *     });
12707 *
12708 * The above snippet will create values like this:
12709 *
12710 *     translateX(11.56px) translateY(11.56px)
12711 *     ...
12712 *     translateX(46.24px) translateY(46.24px)
12713 *     ...
12714 *     translateX(100px) translateY(100px)
12715 *
12716 * In this case, the values for `translateX` and `translateY` are always the
12717 * same for each step of the tween, because they have the same start and end
12718 * points and both use the same easing curve.  We can also tween `translateX`
12719 * and `translateY` along independent curves:
12720 *
12721 *     var tweenable = new NGTweenable();
12722 *     tweenable.tween({
12723 *       from: { transform: 'translateX(0px) translateY(0px)' },
12724 *       to: { transform:   'translateX(100px) translateY(100px)' },
12725 *       easing: { transform: 'easeInQuad bounce' },
12726 *       step: function (state) {
12727 *         console.log(state.transform);
12728 *       }
12729 *     });
12730 *
12731 * The above snippet will create values like this:
12732 *
12733 *     translateX(10.89px) translateY(82.35px)
12734 *     ...
12735 *     translateX(44.89px) translateY(86.73px)
12736 *     ...
12737 *     translateX(100px) translateY(100px)
12738 *
12739 * `translateX` and `translateY` are not in sync anymore, because `easeInQuad`
12740 * was specified for `translateX` and `bounce` for `translateY`.  Mixing and
12741 * matching easing curves can make for some interesting motion in your
12742 * animations.
12743 *
12744 * The order of the space-separated easing curves correspond the token values
12745 * they apply to.  If there are more token values than easing curves listed,
12746 * the last easing curve listed is used.
12747 * @submodule NGTweenable.token
12748 */
12749
12750// token function is defined above only so that dox-foundation sees it as
12751// documentation and renders it.  It is never used, and is optimized away at
12752// build time.
12753
12754;(function (NGTweenable) {
12755
12756  /**
12757   * @typedef {{
12758   *   formatString: string
12759   *   chunkNames: Array.<string>
12760   * }}
12761   * @private
12762   */
12763  var formatManifest;
12764
12765  // CONSTANTS
12766
12767  var R_NUMBER_COMPONENT = /(\d|\-|\.)/;
12768  var R_FORMAT_CHUNKS = /([^\-0-9\.]+)/g;
12769  var R_UNFORMATTED_VALUES = /[0-9.\-]+/g;
12770  var R_RGB = new RegExp(
12771    'rgb\\(' + R_UNFORMATTED_VALUES.source +
12772    (/,\s*/.source) + R_UNFORMATTED_VALUES.source +
12773    (/,\s*/.source) + R_UNFORMATTED_VALUES.source + '\\)', 'g');
12774  var R_RGB_PREFIX = /^.*\(/;
12775  var R_HEX = /#([0-9]|[a-f]){3,6}/gi;
12776  var VALUE_PLACEHOLDER = 'VAL';
12777
12778  // HELPERS
12779
12780  /**
12781   * @param {Array.number} rawValues
12782   * @param {string} prefix
12783   *
12784   * @return {Array.<string>}
12785   * @private
12786   */
vendor: 13,135 bytes, lines 12787-13251
12787  function getFormatChunksFrom (rawValues, prefix) {
12788    var accumulator = [];
12789
12790    var rawValuesLength = rawValues.length;
12791    var i;
12792
12793    for (i = 0; i < rawValuesLength; i++) {
12794      accumulator.push('_' + prefix + '_' + i);
12795    }
12796
12797    return accumulator;
12798  }
12799
12800  /**
12801   * @param {string} formattedString
12802   *
12803   * @return {string}
12804   * @private
12805   */
12806  function getFormatStringFrom (formattedString) {
12807    var chunks = formattedString.match(R_FORMAT_CHUNKS);
12808
12809    if (!chunks) {
12810      // chunks will be null if there were no tokens to parse in
12811      // formattedString (for example, if formattedString is '2').  Coerce
12812      // chunks to be useful here.
12813      chunks = ['', ''];
12814
12815      // If there is only one chunk, assume that the string is a number
12816      // followed by a token...
12817      // NOTE: This may be an unwise assumption.
12818    } else if (chunks.length === 1 ||
12819      // ...or if the string starts with a number component (".", "-", or a
12820      // digit)...
12821    formattedString.charAt(0).match(R_NUMBER_COMPONENT)) {
12822      // ...prepend an empty string here to make sure that the formatted number
12823      // is properly replaced by VALUE_PLACEHOLDER
12824      chunks.unshift('');
12825    }
12826
12827    return chunks.join(VALUE_PLACEHOLDER);
12828  }
12829
12830  /**
12831   * Convert all hex color values within a string to an rgb string.
12832   *
12833   * @param {Object} stateObject
12834   *
12835   * @return {Object} The modified obj
12836   * @private
12837   */
12838  function sanitizeObjectForHexProps (stateObject) {
12839    NGTweenable.each(stateObject, function (prop) {
12840      var currentProp = stateObject[prop];
12841
12842      if (typeof currentProp === 'string' && currentProp.match(R_HEX)) {
12843        stateObject[prop] = sanitizeHexChunksToRGB(currentProp);
12844      }
12845    });
12846  }
12847
12848  /**
12849   * @param {string} str
12850   *
12851   * @return {string}
12852   * @private
12853   */
12854  function  sanitizeHexChunksToRGB (str) {
12855    return filterStringChunks(R_HEX, str, convertHexToRGB);
12856  }
12857
12858  /**
12859   * @param {string} hexString
12860   *
12861   * @return {string}
12862   * @private
12863   */
12864  function convertHexToRGB (hexString) {
12865    var rgbArr = hexToRGBArray(hexString);
12866    return 'rgb(' + rgbArr[0] + ',' + rgbArr[1] + ',' + rgbArr[2] + ')';
12867  }
12868
12869  var hexToRGBArray_returnArray = [];
12870  /**
12871   * Convert a hexadecimal string to an array with three items, one each for
12872   * the red, blue, and green decimal values.
12873   *
12874   * @param {string} hex A hexadecimal string.
12875   *
12876   * @returns {Array.<number>} The converted Array of RGB values if `hex` is a
12877   * valid string, or an Array of three 0's.
12878   * @private
12879   */
12880  function hexToRGBArray (hex) {
12881
12882    hex = hex.replace(/#/, '');
12883
12884    // If the string is a shorthand three digit hex notation, normalize it to
12885    // the standard six digit notation
12886    if (hex.length === 3) {
12887      hex = hex.split('');
12888      hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
12889    }
12890
12891    hexToRGBArray_returnArray[0] = hexToDec(hex.substr(0, 2));
12892    hexToRGBArray_returnArray[1] = hexToDec(hex.substr(2, 2));
12893    hexToRGBArray_returnArray[2] = hexToDec(hex.substr(4, 2));
12894
12895    return hexToRGBArray_returnArray;
12896  }
12897
12898  /**
12899   * Convert a base-16 number to base-10.
12900   *
12901   * @param {Number|String} hex The value to convert
12902   *
12903   * @returns {Number} The base-10 equivalent of `hex`.
12904   * @private
12905   */
12906  function hexToDec (hex) {
12907    return parseInt(hex, 16);
12908  }
12909
12910  /**
12911   * Runs a filter operation on all chunks of a string that match a RegExp
12912   *
12913   * @param {RegExp} pattern
12914   * @param {string} unfilteredString
12915   * @param {function(string)} filter
12916   *
12917   * @return {string}
12918   * @private
12919   */
12920  function filterStringChunks (pattern, unfilteredString, filter) {
12921    var pattenMatches = unfilteredString.match(pattern);
12922    var filteredString = unfilteredString.replace(pattern, VALUE_PLACEHOLDER);
12923
12924    if (pattenMatches) {
12925      var pattenMatchesLength = pattenMatches.length;
12926      var currentChunk;
12927
12928      for (var i = 0; i < pattenMatchesLength; i++) {
12929        currentChunk = pattenMatches.shift();
12930        filteredString = filteredString.replace(
12931          VALUE_PLACEHOLDER, filter(currentChunk));
12932      }
12933    }
12934
12935    return filteredString;
12936  }
12937
12938  /**
12939   * Check for floating point values within rgb strings and rounds them.
12940   *
12941   * @param {string} formattedString
12942   *
12943   * @return {string}
12944   * @private
12945   */
12946  function sanitizeRGBChunks (formattedString) {
12947    return filterStringChunks(R_RGB, formattedString, sanitizeRGBChunk);
12948  }
12949
12950  /**
12951   * @param {string} rgbChunk
12952   *
12953   * @return {string}
12954   * @private
12955   */
12956  function sanitizeRGBChunk (rgbChunk) {
12957    var numbers = rgbChunk.match(R_UNFORMATTED_VALUES);
12958    var numbersLength = numbers.length;
12959    var sanitizedString = rgbChunk.match(R_RGB_PREFIX)[0];
12960
12961    for (var i = 0; i < numbersLength; i++) {
12962      sanitizedString += parseInt(numbers[i], 10) + ',';
12963    }
12964
12965    sanitizedString = sanitizedString.slice(0, -1) + ')';
12966
12967    return sanitizedString;
12968  }
12969
12970  /**
12971   * @param {Object} stateObject
12972   *
12973   * @return {Object} An Object of formatManifests that correspond to
12974   * the string properties of stateObject
12975   * @private
12976   */
12977  function getFormatManifests (stateObject) {
12978    var manifestAccumulator = {};
12979
12980    NGTweenable.each(stateObject, function (prop) {
12981      var currentProp = stateObject[prop];
12982
12983      if (typeof currentProp === 'string') {
12984        var rawValues = getValuesFrom(currentProp);
12985
12986        manifestAccumulator[prop] = {
12987          'formatString': getFormatStringFrom(currentProp)
12988          ,'chunkNames': getFormatChunksFrom(rawValues, prop)
12989        };
12990      }
12991    });
12992
12993    return manifestAccumulator;
12994  }
12995
12996  /**
12997   * @param {Object} stateObject
12998   * @param {Object} formatManifests
12999   * @private
13000   */
13001  function expandFormattedProperties (stateObject, formatManifests) {
13002    NGTweenable.each(formatManifests, function (prop) {
13003      var currentProp = stateObject[prop];
13004      var rawValues = getValuesFrom(currentProp);
13005      var rawValuesLength = rawValues.length;
13006
13007      for (var i = 0; i < rawValuesLength; i++) {
13008        stateObject[formatManifests[prop].chunkNames[i]] = +rawValues[i];
13009      }
13010
13011      delete stateObject[prop];
13012    });
13013  }
13014
13015  /**
13016   * @param {Object} stateObject
13017   * @param {Object} formatManifests
13018   * @private
13019   */
13020  function collapseFormattedProperties (stateObject, formatManifests) {
13021    NGTweenable.each(formatManifests, function (prop) {
13022      var currentProp = stateObject[prop];
13023      var formatChunks = extractPropertyChunks(
13024        stateObject, formatManifests[prop].chunkNames);
13025      var valuesList = getValuesList(
13026        formatChunks, formatManifests[prop].chunkNames);
13027      currentProp = getFormattedValues(
13028        formatManifests[prop].formatString, valuesList);
13029      stateObject[prop] = sanitizeRGBChunks(currentProp);
13030    });
13031  }
13032
13033  /**
13034   * @param {Object} stateObject
13035   * @param {Array.<string>} chunkNames
13036   *
13037   * @return {Object} The extracted value chunks.
13038   * @private
13039   */
13040  function extractPropertyChunks (stateObject, chunkNames) {
13041    var extractedValues = {};
13042    var currentChunkName, chunkNamesLength = chunkNames.length;
13043
13044    for (var i = 0; i < chunkNamesLength; i++) {
13045      currentChunkName = chunkNames[i];
13046      extractedValues[currentChunkName] = stateObject[currentChunkName];
13047      delete stateObject[currentChunkName];
13048    }
13049
13050    return extractedValues;
13051  }
13052
13053  var getValuesList_accumulator = [];
13054  /**
13055   * @param {Object} stateObject
13056   * @param {Array.<string>} chunkNames
13057   *
13058   * @return {Array.<number>}
13059   * @private
13060   */
13061  function getValuesList (stateObject, chunkNames) {
13062    getValuesList_accumulator.length = 0;
13063    var chunkNamesLength = chunkNames.length;
13064
13065    for (var i = 0; i < chunkNamesLength; i++) {
13066      getValuesList_accumulator.push(stateObject[chunkNames[i]]);
13067    }
13068
13069    return getValuesList_accumulator;
13070  }
13071
13072  /**
13073   * @param {string} formatString
13074   * @param {Array.<number>} rawValues
13075   *
13076   * @return {string}
13077   * @private
13078   */
13079  function getFormattedValues (formatString, rawValues) {
13080    var formattedValueString = formatString;
13081    var rawValuesLength = rawValues.length;
13082
13083    for (var i = 0; i < rawValuesLength; i++) {
13084      formattedValueString = formattedValueString.replace(
13085        VALUE_PLACEHOLDER, +rawValues[i].toFixed(4));
13086    }
13087
13088    return formattedValueString;
13089  }
13090
13091  /**
13092   * Note: It's the duty of the caller to convert the Array elements of the
13093   * return value into numbers.  This is a performance optimization.
13094   *
13095   * @param {string} formattedString
13096   *
13097   * @return {Array.<string>|null}
13098   * @private
13099   */
13100  function getValuesFrom (formattedString) {
13101    return formattedString.match(R_UNFORMATTED_VALUES);
13102  }
13103
13104  /**
13105   * @param {Object} easingObject
13106   * @param {Object} tokenData
13107   * @private
13108   */
13109  function expandEasingObject (easingObject, tokenData) {
13110    NGTweenable.each(tokenData, function (prop) {
13111      var currentProp = tokenData[prop];
13112      var chunkNames = currentProp.chunkNames;
13113      var chunkLength = chunkNames.length;
13114
13115      var easing = easingObject[prop];
13116      var i;
13117
13118      if (typeof easing === 'string') {
13119        var easingChunks = easing.split(' ');
13120        var lastEasingChunk = easingChunks[easingChunks.length - 1];
13121
13122        for (i = 0; i < chunkLength; i++) {
13123          easingObject[chunkNames[i]] = easingChunks[i] || lastEasingChunk;
13124        }
13125
13126      } else {
13127        for (i = 0; i < chunkLength; i++) {
13128          easingObject[chunkNames[i]] = easing;
13129        }
13130      }
13131
13132      delete easingObject[prop];
13133    });
13134  }
13135
13136  /**
13137   * @param {Object} easingObject
13138   * @param {Object} tokenData
13139   * @private
13140   */
13141  function collapseEasingObject (easingObject, tokenData) {
13142    NGTweenable.each(tokenData, function (prop) {
13143      var currentProp = tokenData[prop];
13144      var chunkNames = currentProp.chunkNames;
13145      var chunkLength = chunkNames.length;
13146
13147      var firstEasing = easingObject[chunkNames[0]];
13148      var typeofEasings = typeof firstEasing;
13149
13150      if (typeofEasings === 'string') {
13151        var composedEasingString = '';
13152
13153        for (var i = 0; i < chunkLength; i++) {
13154          composedEasingString += ' ' + easingObject[chunkNames[i]];
13155          delete easingObject[chunkNames[i]];
13156        }
13157
13158        easingObject[prop] = composedEasingString.substr(1);
13159      } else {
13160        easingObject[prop] = firstEasing;
13161      }
13162    });
13163  }
13164
13165  NGTweenable.prototype.filter.token = {
13166    'tweenCreated': function (currentState, fromState, toState, easingObject) {
13167      sanitizeObjectForHexProps(currentState);
13168      sanitizeObjectForHexProps(fromState);
13169      sanitizeObjectForHexProps(toState);
13170      this._tokenData = getFormatManifests(currentState);
13171    },
13172
13173    'beforeTween': function (currentState, fromState, toState, easingObject) {
13174      expandEasingObject(easingObject, this._tokenData);
13175      expandFormattedProperties(currentState, this._tokenData);
13176      expandFormattedProperties(fromState, this._tokenData);
13177      expandFormattedProperties(toState, this._tokenData);
13178    },
13179
13180    'afterTween': function (currentState, fromState, toState, easingObject) {
13181      collapseFormattedProperties(currentState, this._tokenData);
13182      collapseFormattedProperties(fromState, this._tokenData);
13183      collapseFormattedProperties(toState, this._tokenData);
13184      collapseEasingObject(easingObject, this._tokenData);
13185    }
13186  };
13187
13188} (NGTweenable));
13189
13190}).call(null);
13191
13192
13193
13194
13195//##########################################################################################################################
13196//## HAMMER.JS #############################################################################################################
13197//##########################################################################################################################
13198
13199// HAMMER.JS
13200
13201// external module EMBEDED in nanogallery
13202// NGY BUILD:
13203// replace "Hammer" with "NGHammer" (case sensitive)
13204// replace "var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined;" with "var SUPPORT_POINTER_EVENTS = false;"
13205// replace "define.amd" with "define.amdDISABLED"
13206
13207
13208
13209/*! NGHammer.JS - v2.0.7 - 2016-04-22
13210 * http://hammerjs.github.io/
13211 *
13212 * Copyright (c) 2016 Jorik Tangelder;
13213 * Licensed under the MIT license */
13214(function(window, document, exportName, undefined) {
13215  'use strict';
13216
13217var VENDOR_PREFIXES = ['', 'webkit', 'Moz', 'MS', 'ms', 'o'];
13218var TEST_ELEMENT = document.createElement('div');
13219
13220var TYPE_FUNCTION = 'function';
13221
13222var round = Math.round;
13223var abs = Math.abs;
13224var now = Date.now;
13225
13226/**
13227 * set a timeout with a given scope
13228 * @param {Function} fn
13229 * @param {Number} timeout
13230 * @param {Object} context
13231 * @returns {number}
13232 */
13233function setTimeoutContext(fn, timeout, context) {
13234    return setTimeout(bindFn(fn, context), timeout);
13235}
13236
13237/**
13238 * if the argument is an array, we want to execute the fn on each entry
13239 * if it aint an array we don't want to do a thing.
13240 * this is used by all the methods that accept a single and array argument.
13241 * @param {*|Array} arg
13242 * @param {String} fn
13243 * @param {Object} [context]
13244 * @returns {Boolean}
13245 */
13246function invokeArrayArg(arg, fn, context) {
13247    if (Array.isArray(arg)) {
13248        each(arg, context[fn], context);
13249        return true;
13250    }
13251    return false;
vendor: 5,340 bytes, lines 13252-13460
13252}
13253
13254/**
13255 * walk objects and arrays
13256 * @param {Object} obj
13257 * @param {Function} iterator
13258 * @param {Object} context
13259 */
13260function each(obj, iterator, context) {
13261    var i;
13262
13263    if (!obj) {
13264        return;
13265    }
13266
13267    if (obj.forEach) {
13268        obj.forEach(iterator, context);
13269    } else if (obj.length !== undefined) {
13270        i = 0;
13271        while (i < obj.length) {
13272            iterator.call(context, obj[i], i, obj);
13273            i++;
13274        }
13275    } else {
13276        for (i in obj) {
13277            obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj);
13278        }
13279    }
13280}
13281
13282/**
13283 * wrap a method with a deprecation warning and stack trace
13284 * @param {Function} method
13285 * @param {String} name
13286 * @param {String} message
13287 * @returns {Function} A new function wrapping the supplied method.
13288 */
13289function deprecate(method, name, message) {
13290    var deprecationMessage = 'DEPRECATED METHOD: ' + name + '\n' + message + ' AT \n';
13291    return function() {
13292        var e = new Error('get-stack-trace');
13293        var stack = e && e.stack ? e.stack.replace(/^[^\(]+?[\n$]/gm, '')
13294            .replace(/^\s+at\s+/gm, '')
13295            .replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@') : 'Unknown Stack Trace';
13296
13297        var log = window.console && (window.console.warn || window.console.log);
13298        if (log) {
13299            log.call(window.console, deprecationMessage, stack);
13300        }
13301        return method.apply(this, arguments);
13302    };
13303}
13304
13305/**
13306 * extend object.
13307 * means that properties in dest will be overwritten by the ones in src.
13308 * @param {Object} target
13309 * @param {...Object} objects_to_assign
13310 * @returns {Object} target
13311 */
13312var assign;
13313if (typeof Object.assign !== 'function') {
13314    assign = function assign(target) {
13315        if (target === undefined || target === null) {
13316            throw new TypeError('Cannot convert undefined or null to object');
13317        }
13318
13319        var output = Object(target);
13320        for (var index = 1; index < arguments.length; index++) {
13321            var source = arguments[index];
13322            if (source !== undefined && source !== null) {
13323                for (var nextKey in source) {
13324                    if (source.hasOwnProperty(nextKey)) {
13325                        output[nextKey] = source[nextKey];
13326                    }
13327                }
13328            }
13329        }
13330        return output;
13331    };
13332} else {
13333    assign = Object.assign;
13334}
13335
13336/**
13337 * extend object.
13338 * means that properties in dest will be overwritten by the ones in src.
13339 * @param {Object} dest
13340 * @param {Object} src
13341 * @param {Boolean} [merge=false]
13342 * @returns {Object} dest
13343 */
13344var extend = deprecate(function extend(dest, src, merge) {
13345    var keys = Object.keys(src);
13346    var i = 0;
13347    while (i < keys.length) {
13348        if (!merge || (merge && dest[keys[i]] === undefined)) {
13349            dest[keys[i]] = src[keys[i]];
13350        }
13351        i++;
13352    }
13353    return dest;
13354}, 'extend', 'Use `assign`.');
13355
13356/**
13357 * merge the values from src in the dest.
13358 * means that properties that exist in dest will not be overwritten by src
13359 * @param {Object} dest
13360 * @param {Object} src
13361 * @returns {Object} dest
13362 */
13363var merge = deprecate(function merge(dest, src) {
13364    return extend(dest, src, true);
13365}, 'merge', 'Use `assign`.');
13366
13367/**
13368 * simple class inheritance
13369 * @param {Function} child
13370 * @param {Function} base
13371 * @param {Object} [properties]
13372 */
13373function inherit(child, base, properties) {
13374    var baseP = base.prototype,
13375        childP;
13376
13377    childP = child.prototype = Object.create(baseP);
13378    childP.constructor = child;
13379    childP._super = baseP;
13380
13381    if (properties) {
13382        assign(childP, properties);
13383    }
13384}
13385
13386/**
13387 * simple function bind
13388 * @param {Function} fn
13389 * @param {Object} context
13390 * @returns {Function}
13391 */
13392function bindFn(fn, context) {
13393    return function boundFn() {
13394        return fn.apply(context, arguments);
13395    };
13396}
13397
13398/**
13399 * let a boolean value also be a function that must return a boolean
13400 * this first item in args will be used as the context
13401 * @param {Boolean|Function} val
13402 * @param {Array} [args]
13403 * @returns {Boolean}
13404 */
13405function boolOrFn(val, args) {
13406    if (typeof val == TYPE_FUNCTION) {
13407        return val.apply(args ? args[0] || undefined : undefined, args);
13408    }
13409    return val;
13410}
13411
13412/**
13413 * use the val2 when val1 is undefined
13414 * @param {*} val1
13415 * @param {*} val2
13416 * @returns {*}
13417 */
13418function ifUndefined(val1, val2) {
13419    return (val1 === undefined) ? val2 : val1;
13420}
13421
13422/**
13423 * addEventListener with multiple events at once
13424 * @param {EventTarget} target
13425 * @param {String} types
13426 * @param {Function} handler
13427 */
13428function addEventListeners(target, types, handler) {
13429    each(splitStr(types), function(type) {
13430        target.addEventListener(type, handler, false);
13431    });
13432}
13433
13434/**
13435 * removeEventListener with multiple events at once
13436 * @param {EventTarget} target
13437 * @param {String} types
13438 * @param {Function} handler
13439 */
13440function removeEventListeners(target, types, handler) {
13441    each(splitStr(types), function(type) {
13442        target.removeEventListener(type, handler, false);
13443    });
13444}
13445
13446/**
13447 * find if a node is in the given parent
13448 * @method hasParent
13449 * @param {HTMLElement} node
13450 * @param {HTMLElement} parent
13451 * @return {Boolean} found
13452 */
13453function hasParent(node, parent) {
13454    while (node) {
13455        if (node == parent) {
13456            return true;
13457        }
13458        node = node.parentNode;
13459    }
13460    return false;
vendor: 5,301 bytes, lines 13461-13676
13461}
13462
13463/**
13464 * small indexOf wrapper
13465 * @param {String} str
13466 * @param {String} find
13467 * @returns {Boolean} found
13468 */
13469function inStr(str, find) {
13470    return str.indexOf(find) > -1;
13471}
13472
13473/**
13474 * split string on whitespace
13475 * @param {String} str
13476 * @returns {Array} words
13477 */
13478function splitStr(str) {
13479    return str.trim().split(/\s+/g);
13480}
13481
13482/**
13483 * find if a array contains the object using indexOf or a simple polyFill
13484 * @param {Array} src
13485 * @param {String} find
13486 * @param {String} [findByKey]
13487 * @return {Boolean|Number} false when not found, or the index
13488 */
13489function inArray(src, find, findByKey) {
13490    if (src.indexOf && !findByKey) {
13491        return src.indexOf(find);
13492    } else {
13493        var i = 0;
13494        while (i < src.length) {
13495            if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) {
13496                return i;
13497            }
13498            i++;
13499        }
13500        return -1;
13501    }
13502}
13503
13504/**
13505 * convert array-like objects to real arrays
13506 * @param {Object} obj
13507 * @returns {Array}
13508 */
13509function toArray(obj) {
13510    return Array.prototype.slice.call(obj, 0);
13511}
13512
13513/**
13514 * unique array with objects based on a key (like 'id') or just by the array's value
13515 * @param {Array} src [{id:1},{id:2},{id:1}]
13516 * @param {String} [key]
13517 * @param {Boolean} [sort=False]
13518 * @returns {Array} [{id:1},{id:2}]
13519 */
13520function uniqueArray(src, key, sort) {
13521    var results = [];
13522    var values = [];
13523    var i = 0;
13524
13525    while (i < src.length) {
13526        var val = key ? src[i][key] : src[i];
13527        if (inArray(values, val) < 0) {
13528            results.push(src[i]);
13529        }
13530        values[i] = val;
13531        i++;
13532    }
13533
13534    if (sort) {
13535        if (!key) {
13536            results = results.sort();
13537        } else {
13538            results = results.sort(function sortUniqueArray(a, b) {
13539                return a[key] > b[key];
13540            });
13541        }
13542    }
13543
13544    return results;
13545}
13546
13547/**
13548 * get the prefixed property
13549 * @param {Object} obj
13550 * @param {String} property
13551 * @returns {String|Undefined} prefixed
13552 */
13553function prefixed(obj, property) {
13554    var prefix, prop;
13555    var camelProp = property[0].toUpperCase() + property.slice(1);
13556
13557    var i = 0;
13558    while (i < VENDOR_PREFIXES.length) {
13559        prefix = VENDOR_PREFIXES[i];
13560        prop = (prefix) ? prefix + camelProp : property;
13561
13562        if (prop in obj) {
13563            return prop;
13564        }
13565        i++;
13566    }
13567    return undefined;
13568}
13569
13570/**
13571 * get a unique id
13572 * @returns {number} uniqueId
13573 */
13574var _uniqueId = 1;
13575function uniqueId() {
13576    return _uniqueId++;
13577}
13578
13579/**
13580 * get the window object of an element
13581 * @param {HTMLElement} element
13582 * @returns {DocumentView|Window}
13583 */
13584function getWindowForElement(element) {
13585    var doc = element.ownerDocument || element;
13586    return (doc.defaultView || doc.parentWindow || window);
13587}
13588
13589var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
13590
13591var SUPPORT_TOUCH = ('ontouchstart' in window);
13592// var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined;
13593var SUPPORT_POINTER_EVENTS = false;
13594var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent);
13595
13596var INPUT_TYPE_TOUCH = 'touch';
13597var INPUT_TYPE_PEN = 'pen';
13598var INPUT_TYPE_MOUSE = 'mouse';
13599var INPUT_TYPE_KINECT = 'kinect';
13600
13601var COMPUTE_INTERVAL = 25;
13602
13603var INPUT_START = 1;
13604var INPUT_MOVE = 2;
13605var INPUT_END = 4;
13606var INPUT_CANCEL = 8;
13607
13608var DIRECTION_NONE = 1;
13609var DIRECTION_LEFT = 2;
13610var DIRECTION_RIGHT = 4;
13611var DIRECTION_UP = 8;
13612var DIRECTION_DOWN = 16;
13613
13614var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT;
13615var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN;
13616var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;
13617
13618var PROPS_XY = ['x', 'y'];
13619var PROPS_CLIENT_XY = ['clientX', 'clientY'];
13620
13621/**
13622 * create new input type manager
13623 * @param {Manager} manager
13624 * @param {Function} callback
13625 * @returns {Input}
13626 * @constructor
13627 */
13628function Input(manager, callback) {
13629    var self = this;
13630    this.manager = manager;
13631    this.callback = callback;
13632    this.element = manager.element;
13633    this.target = manager.options.inputTarget;
13634
13635    // smaller wrapper around the handler, for the scope and the enabled state of the manager,
13636    // so when disabled the input events are completely bypassed.
13637    this.domHandler = function(ev) {
13638        if (boolOrFn(manager.options.enable, [manager])) {
13639            self.handler(ev);
13640        }
13641    };
13642
13643    this.init();
13644
13645}
13646
13647Input.prototype = {
13648    /**
13649     * should handle the inputEvent data and trigger the callback
13650     * @virtual
13651     */
13652    handler: function() { },
13653
13654    /**
13655     * bind the events
13656     */
13657    init: function() {
13658        this.evEl && addEventListeners(this.element, this.evEl, this.domHandler);
13659        this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler);
13660        this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);
13661    },
13662
13663    /**
13664     * unbind the events
13665     */
13666    destroy: function() {
13667        this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler);
13668        this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler);
13669        this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);
13670    }
13671};
13672
13673/**
13674 * create new input type manager
13675 * called by the Manager constructor
13676 * @param {NG
vendor: 17,717 bytes, lines 13676-14327
13676Hammer} manager
13677 * @returns {Input}
13678 */
13679function createInputInstance(manager) {
13680    var Type;
13681    var inputClass = manager.options.inputClass;
13682
13683    if (inputClass) {
13684        Type = inputClass;
13685    } else if (SUPPORT_POINTER_EVENTS) {
13686        Type = PointerEventInput;
13687    } else if (SUPPORT_ONLY_TOUCH) {
13688        Type = TouchInput;
13689    } else if (!SUPPORT_TOUCH) {
13690        Type = MouseInput;
13691    } else {
13692        Type = TouchMouseInput;
13693    }
13694    return new (Type)(manager, inputHandler);
13695}
13696
13697/**
13698 * handle input events
13699 * @param {Manager} manager
13700 * @param {String} eventType
13701 * @param {Object} input
13702 */
13703function inputHandler(manager, eventType, input) {
13704    var pointersLen = input.pointers.length;
13705    var changedPointersLen = input.changedPointers.length;
13706    var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0));
13707    var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0));
13708
13709    input.isFirst = !!isFirst;
13710    input.isFinal = !!isFinal;
13711
13712    if (isFirst) {
13713        manager.session = {};
13714    }
13715
13716    // source event is the normalized value of the domEvents
13717    // like 'touchstart, mouseup, pointerdown'
13718    input.eventType = eventType;
13719
13720    // compute scale, rotation etc
13721    computeInputData(manager, input);
13722
13723    // emit secret event
13724    manager.emit('hammer.input', input);
13725
13726    manager.recognize(input);
13727    manager.session.prevInput = input;
13728}
13729
13730/**
13731 * extend the data with some usable properties like scale, rotate, velocity etc
13732 * @param {Object} manager
13733 * @param {Object} input
13734 */
13735function computeInputData(manager, input) {
13736    var session = manager.session;
13737    var pointers = input.pointers;
13738    var pointersLength = pointers.length;
13739
13740    // store the first input to calculate the distance and direction
13741    if (!session.firstInput) {
13742        session.firstInput = simpleCloneInputData(input);
13743    }
13744
13745    // to compute scale and rotation we need to store the multiple touches
13746    if (pointersLength > 1 && !session.firstMultiple) {
13747        session.firstMultiple = simpleCloneInputData(input);
13748    } else if (pointersLength === 1) {
13749        session.firstMultiple = false;
13750    }
13751
13752    var firstInput = session.firstInput;
13753    var firstMultiple = session.firstMultiple;
13754    var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center;
13755
13756    var center = input.center = getCenter(pointers);
13757    input.timeStamp = now();
13758    input.deltaTime = input.timeStamp - firstInput.timeStamp;
13759
13760    input.angle = getAngle(offsetCenter, center);
13761    input.distance = getDistance(offsetCenter, center);
13762
13763    computeDeltaXY(session, input);
13764    input.offsetDirection = getDirection(input.deltaX, input.deltaY);
13765
13766    var overallVelocity = getVelocity(input.deltaTime, input.deltaX, input.deltaY);
13767    input.overallVelocityX = overallVelocity.x;
13768    input.overallVelocityY = overallVelocity.y;
13769    input.overallVelocity = (abs(overallVelocity.x) > abs(overallVelocity.y)) ? overallVelocity.x : overallVelocity.y;
13770
13771    input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1;
13772    input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0;
13773
13774    input.maxPointers = !session.prevInput ? input.pointers.length : ((input.pointers.length >
13775        session.prevInput.maxPointers) ? input.pointers.length : session.prevInput.maxPointers);
13776
13777    computeIntervalInputData(session, input);
13778
13779    // find the correct target
13780    var target = manager.element;
13781    if (hasParent(input.srcEvent.target, target)) {
13782        target = input.srcEvent.target;
13783    }
13784    input.target = target;
13785}
13786
13787function computeDeltaXY(session, input) {
13788    var center = input.center;
13789    var offset = session.offsetDelta || {};
13790    var prevDelta = session.prevDelta || {};
13791    var prevInput = session.prevInput || {};
13792
13793    if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) {
13794        prevDelta = session.prevDelta = {
13795            x: prevInput.deltaX || 0,
13796            y: prevInput.deltaY || 0
13797        };
13798
13799        offset = session.offsetDelta = {
13800            x: center.x,
13801            y: center.y
13802        };
13803    }
13804
13805    input.deltaX = prevDelta.x + (center.x - offset.x);
13806    input.deltaY = prevDelta.y + (center.y - offset.y);
13807}
13808
13809/**
13810 * velocity is calculated every x ms
13811 * @param {Object} session
13812 * @param {Object} input
13813 */
13814function computeIntervalInputData(session, input) {
13815    var last = session.lastInterval || input,
13816        deltaTime = input.timeStamp - last.timeStamp,
13817        velocity, velocityX, velocityY, direction;
13818
13819    if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) {
13820        var deltaX = input.deltaX - last.deltaX;
13821        var deltaY = input.deltaY - last.deltaY;
13822
13823        var v = getVelocity(deltaTime, deltaX, deltaY);
13824        velocityX = v.x;
13825        velocityY = v.y;
13826        velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y;
13827        direction = getDirection(deltaX, deltaY);
13828
13829        session.lastInterval = input;
13830    } else {
13831        // use latest velocity info if it doesn't overtake a minimum period
13832        velocity = last.velocity;
13833        velocityX = last.velocityX;
13834        velocityY = last.velocityY;
13835        direction = last.direction;
13836    }
13837
13838    input.velocity = velocity;
13839    input.velocityX = velocityX;
13840    input.velocityY = velocityY;
13841    input.direction = direction;
13842}
13843
13844/**
13845 * create a simple clone from the input used for storage of firstInput and firstMultiple
13846 * @param {Object} input
13847 * @returns {Object} clonedInputData
13848 */
13849function simpleCloneInputData(input) {
13850    // make a simple copy of the pointers because we will get a reference if we don't
13851    // we only need clientXY for the calculations
13852    var pointers = [];
13853    var i = 0;
13854    while (i < input.pointers.length) {
13855        pointers[i] = {
13856            clientX: round(input.pointers[i].clientX),
13857            clientY: round(input.pointers[i].clientY)
13858        };
13859        i++;
13860    }
13861
13862    return {
13863        timeStamp: now(),
13864        pointers: pointers,
13865        center: getCenter(pointers),
13866        deltaX: input.deltaX,
13867        deltaY: input.deltaY
13868    };
13869}
13870
13871/**
13872 * get the center of all the pointers
13873 * @param {Array} pointers
13874 * @return {Object} center contains `x` and `y` properties
13875 */
13876function getCenter(pointers) {
13877    var pointersLength = pointers.length;
13878
13879    // no need to loop when only one touch
13880    if (pointersLength === 1) {
13881        return {
13882            x: round(pointers[0].clientX),
13883            y: round(pointers[0].clientY)
13884        };
13885    }
13886
13887    var x = 0, y = 0, i = 0;
13888    while (i < pointersLength) {
13889        x += pointers[i].clientX;
13890        y += pointers[i].clientY;
13891        i++;
13892    }
13893
13894    return {
13895        x: round(x / pointersLength),
13896        y: round(y / pointersLength)
13897    };
13898}
13899
13900/**
13901 * calculate the velocity between two points. unit is in px per ms.
13902 * @param {Number} deltaTime
13903 * @param {Number} x
13904 * @param {Number} y
13905 * @return {Object} velocity `x` and `y`
13906 */
13907function getVelocity(deltaTime, x, y) {
13908    return {
13909        x: x / deltaTime || 0,
13910        y: y / deltaTime || 0
13911    };
13912}
13913
13914/**
13915 * get the direction between two points
13916 * @param {Number} x
13917 * @param {Number} y
13918 * @return {Number} direction
13919 */
13920function getDirection(x, y) {
13921    if (x === y) {
13922        return DIRECTION_NONE;
13923    }
13924
13925    if (abs(x) >= abs(y)) {
13926        return x < 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
13927    }
13928    return y < 0 ? DIRECTION_UP : DIRECTION_DOWN;
13929}
13930
13931/**
13932 * calculate the absolute distance between two points
13933 * @param {Object} p1 {x, y}
13934 * @param {Object} p2 {x, y}
13935 * @param {Array} [props] containing x and y keys
13936 * @return {Number} distance
13937 */
13938function getDistance(p1, p2, props) {
13939    if (!props) {
13940        props = PROPS_XY;
13941    }
13942    var x = p2[props[0]] - p1[props[0]],
13943        y = p2[props[1]] - p1[props[1]];
13944
13945    return Math.sqrt((x * x) + (y * y));
13946}
13947
13948/**
13949 * calculate the angle between two coordinates
13950 * @param {Object} p1
13951 * @param {Object} p2
13952 * @param {Array} [props] containing x and y keys
13953 * @return {Number} angle
13954 */
13955function getAngle(p1, p2, props) {
13956    if (!props) {
13957        props = PROPS_XY;
13958    }
13959    var x = p2[props[0]] - p1[props[0]],
13960        y = p2[props[1]] - p1[props[1]];
13961    return Math.atan2(y, x) * 180 / Math.PI;
13962}
13963
13964/**
13965 * calculate the rotation degrees between two pointersets
13966 * @param {Array} start array of pointers
13967 * @param {Array} end array of pointers
13968 * @return {Number} rotation
13969 */
13970function getRotation(start, end) {
13971    return getAngle(end[1], end[0], PROPS_CLIENT_XY) + getAngle(start[1], start[0], PROPS_CLIENT_XY);
13972}
13973
13974/**
13975 * calculate the scale factor between two pointersets
13976 * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
13977 * @param {Array} start array of pointers
13978 * @param {Array} end array of pointers
13979 * @return {Number} scale
13980 */
13981function getScale(start, end) {
13982    return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY);
13983}
13984
13985var MOUSE_INPUT_MAP = {
13986    mousedown: INPUT_START,
13987    mousemove: INPUT_MOVE,
13988    mouseup: INPUT_END
13989};
13990
13991var MOUSE_ELEMENT_EVENTS = 'mousedown';
13992var MOUSE_WINDOW_EVENTS = 'mousemove mouseup';
13993
13994/**
13995 * Mouse events input
13996 * @constructor
13997 * @extends Input
13998 */
13999function MouseInput() {
14000    this.evEl = MOUSE_ELEMENT_EVENTS;
14001    this.evWin = MOUSE_WINDOW_EVENTS;
14002
14003    this.pressed = false; // mousedown state
14004
14005    Input.apply(this, arguments);
14006}
14007
14008inherit(MouseInput, Input, {
14009    /**
14010     * handle mouse events
14011     * @param {Object} ev
14012     */
14013    handler: function MEhandler(ev) {
14014        var eventType = MOUSE_INPUT_MAP[ev.type];
14015
14016        // on start we want to have the left mouse button down
14017        if (eventType & INPUT_START && ev.button === 0) {
14018            this.pressed = true;
14019        }
14020
14021        if (eventType & INPUT_MOVE && ev.which !== 1) {
14022            eventType = INPUT_END;
14023        }
14024
14025        // mouse must be down
14026        if (!this.pressed) {
14027            return;
14028        }
14029
14030        if (eventType & INPUT_END) {
14031            this.pressed = false;
14032        }
14033
14034        this.callback(this.manager, eventType, {
14035            pointers: [ev],
14036            changedPointers: [ev],
14037            pointerType: INPUT_TYPE_MOUSE,
14038            srcEvent: ev
14039        });
14040    }
14041});
14042
14043var POINTER_INPUT_MAP = {
14044    pointerdown: INPUT_START,
14045    pointermove: INPUT_MOVE,
14046    pointerup: INPUT_END,
14047    pointercancel: INPUT_CANCEL,
14048    pointerout: INPUT_CANCEL
14049};
14050
14051// in IE10 the pointer types is defined as an enum
14052var IE10_POINTER_TYPE_ENUM = {
14053    2: INPUT_TYPE_TOUCH,
14054    3: INPUT_TYPE_PEN,
14055    4: INPUT_TYPE_MOUSE,
14056    5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816
14057};
14058
14059var POINTER_ELEMENT_EVENTS = 'pointerdown';
14060var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel';
14061
14062// IE10 has prefixed support, and case-sensitive
14063if (window.MSPointerEvent && !window.PointerEvent) {
14064    POINTER_ELEMENT_EVENTS = 'MSPointerDown';
14065    POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel';
14066}
14067
14068/**
14069 * Pointer events input
14070 * @constructor
14071 * @extends Input
14072 */
14073function PointerEventInput() {
14074    this.evEl = POINTER_ELEMENT_EVENTS;
14075    this.evWin = POINTER_WINDOW_EVENTS;
14076
14077    Input.apply(this, arguments);
14078
14079    this.store = (this.manager.session.pointerEvents = []);
14080}
14081
14082inherit(PointerEventInput, Input, {
14083    /**
14084     * handle mouse events
14085     * @param {Object} ev
14086     */
14087    handler: function PEhandler(ev) {
14088        var store = this.store;
14089        var removePointer = false;
14090
14091        var eventTypeNormalized = ev.type.toLowerCase().replace('ms', '');
14092        var eventType = POINTER_INPUT_MAP[eventTypeNormalized];
14093        var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType;
14094
14095        var isTouch = (pointerType == INPUT_TYPE_TOUCH);
14096
14097        // get index of the event in the store
14098        var storeIndex = inArray(store, ev.pointerId, 'pointerId');
14099
14100        // start and mouse must be down
14101        if (eventType & INPUT_START && (ev.button === 0 || isTouch)) {
14102            if (storeIndex < 0) {
14103                store.push(ev);
14104                storeIndex = store.length - 1;
14105            }
14106        } else if (eventType & (INPUT_END | INPUT_CANCEL)) {
14107            removePointer = true;
14108        }
14109
14110        // it not found, so the pointer hasn't been down (so it's probably a hover)
14111        if (storeIndex < 0) {
14112            return;
14113        }
14114
14115        // update the event in the store
14116        store[storeIndex] = ev;
14117
14118        this.callback(this.manager, eventType, {
14119            pointers: store,
14120            changedPointers: [ev],
14121            pointerType: pointerType,
14122            srcEvent: ev
14123        });
14124
14125        if (removePointer) {
14126            // remove from the store
14127            store.splice(storeIndex, 1);
14128        }
14129    }
14130});
14131
14132var SINGLE_TOUCH_INPUT_MAP = {
14133    touchstart: INPUT_START,
14134    touchmove: INPUT_MOVE,
14135    touchend: INPUT_END,
14136    touchcancel: INPUT_CANCEL
14137};
14138
14139var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart';
14140var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel';
14141
14142/**
14143 * Touch events input
14144 * @constructor
14145 * @extends Input
14146 */
14147function SingleTouchInput() {
14148    this.evTarget = SINGLE_TOUCH_TARGET_EVENTS;
14149    this.evWin = SINGLE_TOUCH_WINDOW_EVENTS;
14150    this.started = false;
14151
14152    Input.apply(this, arguments);
14153}
14154
14155inherit(SingleTouchInput, Input, {
14156    handler: function TEhandler(ev) {
14157        var type = SINGLE_TOUCH_INPUT_MAP[ev.type];
14158
14159        // should we handle the touch events?
14160        if (type === INPUT_START) {
14161            this.started = true;
14162        }
14163
14164        if (!this.started) {
14165            return;
14166        }
14167
14168        var touches = normalizeSingleTouches.call(this, ev, type);
14169
14170        // when done, reset the started state
14171        if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) {
14172            this.started = false;
14173        }
14174
14175        this.callback(this.manager, type, {
14176            pointers: touches[0],
14177            changedPointers: touches[1],
14178            pointerType: INPUT_TYPE_TOUCH,
14179            srcEvent: ev
14180        });
14181    }
14182});
14183
14184/**
14185 * @this {TouchInput}
14186 * @param {Object} ev
14187 * @param {Number} type flag
14188 * @returns {undefined|Array} [all, changed]
14189 */
14190function normalizeSingleTouches(ev, type) {
14191    var all = toArray(ev.touches);
14192    var changed = toArray(ev.changedTouches);
14193
14194    if (type & (INPUT_END | INPUT_CANCEL)) {
14195        all = uniqueArray(all.concat(changed), 'identifier', true);
14196    }
14197
14198    return [all, changed];
14199}
14200
14201var TOUCH_INPUT_MAP = {
14202    touchstart: INPUT_START,
14203    touchmove: INPUT_MOVE,
14204    touchend: INPUT_END,
14205    touchcancel: INPUT_CANCEL
14206};
14207
14208var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel';
14209
14210/**
14211 * Multi-user touch events input
14212 * @constructor
14213 * @extends Input
14214 */
14215function TouchInput() {
14216    this.evTarget = TOUCH_TARGET_EVENTS;
14217    this.targetIds = {};
14218
14219    Input.apply(this, arguments);
14220}
14221
14222inherit(TouchInput, Input, {
14223    handler: function MTEhandler(ev) {
14224        var type = TOUCH_INPUT_MAP[ev.type];
14225        var touches = getTouches.call(this, ev, type);
14226        if (!touches) {
14227            return;
14228        }
14229
14230        this.callback(this.manager, type, {
14231            pointers: touches[0],
14232            changedPointers: touches[1],
14233            pointerType: INPUT_TYPE_TOUCH,
14234            srcEvent: ev
14235        });
14236    }
14237});
14238
14239/**
14240 * @this {TouchInput}
14241 * @param {Object} ev
14242 * @param {Number} type flag
14243 * @returns {undefined|Array} [all, changed]
14244 */
14245function getTouches(ev, type) {
14246    var allTouches = toArray(ev.touches);
14247    var targetIds = this.targetIds;
14248
14249    // when there is only one touch, the process can be simplified
14250    if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) {
14251        targetIds[allTouches[0].identifier] = true;
14252        return [allTouches, allTouches];
14253    }
14254
14255    var i,
14256        targetTouches,
14257        changedTouches = toArray(ev.changedTouches),
14258        changedTargetTouches = [],
14259        target = this.target;
14260
14261    // get target touches from touches
14262    targetTouches = allTouches.filter(function(touch) {
14263        return hasParent(touch.target, target);
14264    });
14265
14266    // collect touches
14267    if (type === INPUT_START) {
14268        i = 0;
14269        while (i < targetTouches.length) {
14270            targetIds[targetTouches[i].identifier] = true;
14271            i++;
14272        }
14273    }
14274
14275    // filter changed touches to only contain touches that exist in the collected target ids
14276    i = 0;
14277    while (i < changedTouches.length) {
14278        if (targetIds[changedTouches[i].identifier]) {
14279            changedTargetTouches.push(changedTouches[i]);
14280        }
14281
14282        // cleanup removed touches
14283        if (type & (INPUT_END | INPUT_CANCEL)) {
14284            delete targetIds[changedTouches[i].identifier];
14285        }
14286        i++;
14287    }
14288
14289    if (!changedTargetTouches.length) {
14290        return;
14291    }
14292
14293    return [
14294        // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel'
14295        uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true),
14296        changedTargetTouches
14297    ];
14298}
14299
14300/**
14301 * Combined touch and mouse input
14302 *
14303 * Touch has a higher priority then mouse, and while touching no mouse events are allowed.
14304 * This because touch devices also emit mouse events while doing a touch.
14305 *
14306 * @constructor
14307 * @extends Input
14308 */
14309
14310var DEDUP_TIMEOUT = 2500;
14311var DEDUP_DISTANCE = 25;
14312
14313function TouchMouseInput() {
14314    Input.apply(this, arguments);
14315
14316    var handler = bindFn(this.handler, this);
14317    this.touch = new TouchInput(this.manager, handler);
14318    this.mouse = new MouseInput(this.manager, handler);
14319
14320    this.primaryTouch = null;
14321    this.lastTouches = [];
14322}
14323
14324inherit(TouchMouseInput, Input, {
14325    /**
14326     * handle mouse and touch events
14327     * @param {
14327NG
vendor: 29,735 bytes, lines 14327-15348
14327Hammer} manager
14328     * @param {String} inputEvent
14329     * @param {Object} inputData
14330     */
14331    handler: function TMEhandler(manager, inputEvent, inputData) {
14332        var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH),
14333            isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE);
14334
14335        if (isMouse && inputData.sourceCapabilities && inputData.sourceCapabilities.firesTouchEvents) {
14336            return;
14337        }
14338
14339        // when we're in a touch event, record touches to  de-dupe synthetic mouse event
14340        if (isTouch) {
14341            recordTouches.call(this, inputEvent, inputData);
14342        } else if (isMouse && isSyntheticEvent.call(this, inputData)) {
14343            return;
14344        }
14345
14346        this.callback(manager, inputEvent, inputData);
14347    },
14348
14349    /**
14350     * remove the event listeners
14351     */
14352    destroy: function destroy() {
14353        this.touch.destroy();
14354        this.mouse.destroy();
14355    }
14356});
14357
14358function recordTouches(eventType, eventData) {
14359    if (eventType & INPUT_START) {
14360        this.primaryTouch = eventData.changedPointers[0].identifier;
14361        setLastTouch.call(this, eventData);
14362    } else if (eventType & (INPUT_END | INPUT_CANCEL)) {
14363        setLastTouch.call(this, eventData);
14364    }
14365}
14366
14367function setLastTouch(eventData) {
14368    var touch = eventData.changedPointers[0];
14369
14370    if (touch.identifier === this.primaryTouch) {
14371        var lastTouch = {x: touch.clientX, y: touch.clientY};
14372        this.lastTouches.push(lastTouch);
14373        var lts = this.lastTouches;
14374        var removeLastTouch = function() {
14375            var i = lts.indexOf(lastTouch);
14376            if (i > -1) {
14377                lts.splice(i, 1);
14378            }
14379        };
14380        setTimeout(removeLastTouch, DEDUP_TIMEOUT);
14381    }
14382}
14383
14384function isSyntheticEvent(eventData) {
14385    var x = eventData.srcEvent.clientX, y = eventData.srcEvent.clientY;
14386    for (var i = 0; i < this.lastTouches.length; i++) {
14387        var t = this.lastTouches[i];
14388        var dx = Math.abs(x - t.x), dy = Math.abs(y - t.y);
14389        if (dx <= DEDUP_DISTANCE && dy <= DEDUP_DISTANCE) {
14390            return true;
14391        }
14392    }
14393    return false;
14394}
14395
14396var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction');
14397var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined;
14398
14399// magical touchAction value
14400var TOUCH_ACTION_COMPUTE = 'compute';
14401var TOUCH_ACTION_AUTO = 'auto';
14402var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented
14403var TOUCH_ACTION_NONE = 'none';
14404var TOUCH_ACTION_PAN_X = 'pan-x';
14405var TOUCH_ACTION_PAN_Y = 'pan-y';
14406var TOUCH_ACTION_MAP = getTouchActionProps();
14407
14408/**
14409 * Touch Action
14410 * sets the touchAction property or uses the js alternative
14411 * @param {Manager} manager
14412 * @param {String} value
14413 * @constructor
14414 */
14415function TouchAction(manager, value) {
14416    this.manager = manager;
14417    this.set(value);
14418}
14419
14420TouchAction.prototype = {
14421    /**
14422     * set the touchAction value on the element or enable the polyfill
14423     * @param {String} value
14424     */
14425    set: function(value) {
14426        // find out the touch-action by the event handlers
14427        if (value == TOUCH_ACTION_COMPUTE) {
14428            value = this.compute();
14429        }
14430
14431        if (NATIVE_TOUCH_ACTION && this.manager.element.style && TOUCH_ACTION_MAP[value]) {
14432            this.manager.element.style[PREFIXED_TOUCH_ACTION] = value;
14433        }
14434        this.actions = value.toLowerCase().trim();
14435    },
14436
14437    /**
14438     * just re-set the touchAction value
14439     */
14440    update: function() {
14441        this.set(this.manager.options.touchAction);
14442    },
14443
14444    /**
14445     * compute the value for the touchAction property based on the recognizer's settings
14446     * @returns {String} value
14447     */
14448    compute: function() {
14449        var actions = [];
14450        each(this.manager.recognizers, function(recognizer) {
14451            if (boolOrFn(recognizer.options.enable, [recognizer])) {
14452                actions = actions.concat(recognizer.getTouchAction());
14453            }
14454        });
14455        return cleanTouchActions(actions.join(' '));
14456    },
14457
14458    /**
14459     * this method is called on each input cycle and provides the preventing of the browser behavior
14460     * @param {Object} input
14461     */
14462    preventDefaults: function(input) {
14463        var srcEvent = input.srcEvent;
14464        var direction = input.offsetDirection;
14465
14466        // if the touch action did prevented once this session
14467        if (this.manager.session.prevented) {
14468            srcEvent.preventDefault();
14469            return;
14470        }
14471
14472        var actions = this.actions;
14473        var hasNone = inStr(actions, TOUCH_ACTION_NONE) && !TOUCH_ACTION_MAP[TOUCH_ACTION_NONE];
14474        var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y) && !TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_Y];
14475        var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X) && !TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_X];
14476
14477        if (hasNone) {
14478            //do not prevent defaults if this is a tap gesture
14479
14480            var isTapPointer = input.pointers.length === 1;
14481            var isTapMovement = input.distance < 2;
14482            var isTapTouchTime = input.deltaTime < 250;
14483
14484            if (isTapPointer && isTapMovement && isTapTouchTime) {
14485                return;
14486            }
14487        }
14488
14489        if (hasPanX && hasPanY) {
14490            // `pan-x pan-y` means browser handles all scrolling/panning, do not prevent
14491            return;
14492        }
14493
14494        if (hasNone ||
14495            (hasPanY && direction & DIRECTION_HORIZONTAL) ||
14496            (hasPanX && direction & DIRECTION_VERTICAL)) {
14497            return this.preventSrc(srcEvent);
14498        }
14499    },
14500
14501    /**
14502     * call preventDefault to prevent the browser's default behavior (scrolling in most cases)
14503     * @param {Object} srcEvent
14504     */
14505    preventSrc: function(srcEvent) {
14506        this.manager.session.prevented = true;
14507        srcEvent.preventDefault();
14508    }
14509};
14510
14511/**
14512 * when the touchActions are collected they are not a valid value, so we need to clean things up. *
14513 * @param {String} actions
14514 * @returns {*}
14515 */
14516function cleanTouchActions(actions) {
14517    // none
14518    if (inStr(actions, TOUCH_ACTION_NONE)) {
14519        return TOUCH_ACTION_NONE;
14520    }
14521
14522    var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X);
14523    var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y);
14524
14525    // if both pan-x and pan-y are set (different recognizers
14526    // for different directions, e.g. horizontal pan but vertical swipe?)
14527    // we need none (as otherwise with pan-x pan-y combined none of these
14528    // recognizers will work, since the browser would handle all panning
14529    if (hasPanX && hasPanY) {
14530        return TOUCH_ACTION_NONE;
14531    }
14532
14533    // pan-x OR pan-y
14534    if (hasPanX || hasPanY) {
14535        return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y;
14536    }
14537
14538    // manipulation
14539    if (inStr(actions, TOUCH_ACTION_MANIPULATION)) {
14540        return TOUCH_ACTION_MANIPULATION;
14541    }
14542
14543    return TOUCH_ACTION_AUTO;
14544}
14545
14546function getTouchActionProps() {
14547    if (!NATIVE_TOUCH_ACTION) {
14548        return false;
14549    }
14550    var touchMap = {};
14551    var cssSupports = window.CSS && window.CSS.supports;
14552    ['auto', 'manipulation', 'pan-y', 'pan-x', 'pan-x pan-y', 'none'].forEach(function(val) {
14553
14554        // If css.supports is not supported but there is native touch-action assume it supports
14555        // all values. This is the case for IE 10 and 11.
14556        touchMap[val] = cssSupports ? window.CSS.supports('touch-action', val) : true;
14557    });
14558    return touchMap;
14559}
14560
14561/**
14562 * Recognizer flow explained; *
14563 * All recognizers have the initial state of POSSIBLE when a input session starts.
14564 * The definition of a input session is from the first input until the last input, with all it's movement in it. *
14565 * Example session for mouse-input: mousedown -> mousemove -> mouseup
14566 *
14567 * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed
14568 * which determines with state it should be.
14569 *
14570 * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to
14571 * POSSIBLE to give it another change on the next cycle.
14572 *
14573 *               Possible
14574 *                  |
14575 *            +-----+---------------+
14576 *            |                     |
14577 *      +-----+-----+               |
14578 *      |           |               |
14579 *   Failed      Cancelled          |
14580 *                          +-------+------+
14581 *                          |              |
14582 *                      Recognized       Began
14583 *                                         |
14584 *                                      Changed
14585 *                                         |
14586 *                                  Ended/Recognized
14587 */
14588var STATE_POSSIBLE = 1;
14589var STATE_BEGAN = 2;
14590var STATE_CHANGED = 4;
14591var STATE_ENDED = 8;
14592var STATE_RECOGNIZED = STATE_ENDED;
14593var STATE_CANCELLED = 16;
14594var STATE_FAILED = 32;
14595
14596/**
14597 * Recognizer
14598 * Every recognizer needs to extend from this class.
14599 * @constructor
14600 * @param {Object} options
14601 */
14602function Recognizer(options) {
14603    this.options = assign({}, this.defaults, options || {});
14604
14605    this.id = uniqueId();
14606
14607    this.manager = null;
14608
14609    // default is enable true
14610    this.options.enable = ifUndefined(this.options.enable, true);
14611
14612    this.state = STATE_POSSIBLE;
14613
14614    this.simultaneous = {};
14615    this.requireFail = [];
14616}
14617
14618Recognizer.prototype = {
14619    /**
14620     * @virtual
14621     * @type {Object}
14622     */
14623    defaults: {},
14624
14625    /**
14626     * set options
14627     * @param {Object} options
14628     * @return {Recognizer}
14629     */
14630    set: function(options) {
14631        assign(this.options, options);
14632
14633        // also update the touchAction, in case something changed about the directions/enabled state
14634        this.manager && this.manager.touchAction.update();
14635        return this;
14636    },
14637
14638    /**
14639     * recognize simultaneous with an other recognizer.
14640     * @param {Recognizer} otherRecognizer
14641     * @returns {Recognizer} this
14642     */
14643    recognizeWith: function(otherRecognizer) {
14644        if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) {
14645            return this;
14646        }
14647
14648        var simultaneous = this.simultaneous;
14649        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
14650        if (!simultaneous[otherRecognizer.id]) {
14651            simultaneous[otherRecognizer.id] = otherRecognizer;
14652            otherRecognizer.recognizeWith(this);
14653        }
14654        return this;
14655    },
14656
14657    /**
14658     * drop the simultaneous link. it doesnt remove the link on the other recognizer.
14659     * @param {Recognizer} otherRecognizer
14660     * @returns {Recognizer} this
14661     */
14662    dropRecognizeWith: function(otherRecognizer) {
14663        if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) {
14664            return this;
14665        }
14666
14667        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
14668        delete this.simultaneous[otherRecognizer.id];
14669        return this;
14670    },
14671
14672    /**
14673     * recognizer can only run when an other is failing
14674     * @param {Recognizer} otherRecognizer
14675     * @returns {Recognizer} this
14676     */
14677    requireFailure: function(otherRecognizer) {
14678        if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) {
14679            return this;
14680        }
14681
14682        var requireFail = this.requireFail;
14683        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
14684        if (inArray(requireFail, otherRecognizer) === -1) {
14685            requireFail.push(otherRecognizer);
14686            otherRecognizer.requireFailure(this);
14687        }
14688        return this;
14689    },
14690
14691    /**
14692     * drop the requireFailure link. it does not remove the link on the other recognizer.
14693     * @param {Recognizer} otherRecognizer
14694     * @returns {Recognizer} this
14695     */
14696    dropRequireFailure: function(otherRecognizer) {
14697        if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) {
14698            return this;
14699        }
14700
14701        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
14702        var index = inArray(this.requireFail, otherRecognizer);
14703        if (index > -1) {
14704            this.requireFail.splice(index, 1);
14705        }
14706        return this;
14707    },
14708
14709    /**
14710     * has require failures boolean
14711     * @returns {boolean}
14712     */
14713    hasRequireFailures: function() {
14714        return this.requireFail.length > 0;
14715    },
14716
14717    /**
14718     * if the recognizer can recognize simultaneous with an other recognizer
14719     * @param {Recognizer} otherRecognizer
14720     * @returns {Boolean}
14721     */
14722    canRecognizeWith: function(otherRecognizer) {
14723        return !!this.simultaneous[otherRecognizer.id];
14724    },
14725
14726    /**
14727     * You should use `tryEmit` instead of `emit` directly to check
14728     * that all the needed recognizers has failed before emitting.
14729     * @param {Object} input
14730     */
14731    emit: function(input) {
14732        var self = this;
14733        var state = this.state;
14734
14735        function emit(event) {
14736            self.manager.emit(event, input);
14737        }
14738
14739        // 'panstart' and 'panmove'
14740        if (state < STATE_ENDED) {
14741            emit(self.options.event + stateStr(state));
14742        }
14743
14744        emit(self.options.event); // simple 'eventName' events
14745
14746        if (input.additionalEvent) { // additional event(panleft, panright, pinchin, pinchout...)
14747            emit(input.additionalEvent);
14748        }
14749
14750        // panend and pancancel
14751        if (state >= STATE_ENDED) {
14752            emit(self.options.event + stateStr(state));
14753        }
14754    },
14755
14756    /**
14757     * Check that all the require failure recognizers has failed,
14758     * if true, it emits a gesture event,
14759     * otherwise, setup the state to FAILED.
14760     * @param {Object} input
14761     */
14762    tryEmit: function(input) {
14763        if (this.canEmit()) {
14764            return this.emit(input);
14765        }
14766        // it's failing anyway
14767        this.state = STATE_FAILED;
14768    },
14769
14770    /**
14771     * can we emit?
14772     * @returns {boolean}
14773     */
14774    canEmit: function() {
14775        var i = 0;
14776        while (i < this.requireFail.length) {
14777            if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) {
14778                return false;
14779            }
14780            i++;
14781        }
14782        return true;
14783    },
14784
14785    /**
14786     * update the recognizer
14787     * @param {Object} inputData
14788     */
14789    recognize: function(inputData) {
14790        // make a new copy of the inputData
14791        // so we can change the inputData without messing up the other recognizers
14792        var inputDataClone = assign({}, inputData);
14793
14794        // is is enabled and allow recognizing?
14795        if (!boolOrFn(this.options.enable, [this, inputDataClone])) {
14796            this.reset();
14797            this.state = STATE_FAILED;
14798            return;
14799        }
14800
14801        // reset when we've reached the end
14802        if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) {
14803            this.state = STATE_POSSIBLE;
14804        }
14805
14806        this.state = this.process(inputDataClone);
14807
14808        // the recognizer has recognized a gesture
14809        // so trigger an event
14810        if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) {
14811            this.tryEmit(inputDataClone);
14812        }
14813    },
14814
14815    /**
14816     * return the state of the recognizer
14817     * the actual recognizing happens in this method
14818     * @virtual
14819     * @param {Object} inputData
14820     * @returns {Const} STATE
14821     */
14822    process: function(inputData) { }, // jshint ignore:line
14823
14824    /**
14825     * return the preferred touch-action
14826     * @virtual
14827     * @returns {Array}
14828     */
14829    getTouchAction: function() { },
14830
14831    /**
14832     * called when the gesture isn't allowed to recognize
14833     * like when another is being recognized or it is disabled
14834     * @virtual
14835     */
14836    reset: function() { }
14837};
14838
14839/**
14840 * get a usable string, used as event postfix
14841 * @param {Const} state
14842 * @returns {String} state
14843 */
14844function stateStr(state) {
14845    if (state & STATE_CANCELLED) {
14846        return 'cancel';
14847    } else if (state & STATE_ENDED) {
14848        return 'end';
14849    } else if (state & STATE_CHANGED) {
14850        return 'move';
14851    } else if (state & STATE_BEGAN) {
14852        return 'start';
14853    }
14854    return '';
14855}
14856
14857/**
14858 * direction cons to string
14859 * @param {Const} direction
14860 * @returns {String}
14861 */
14862function directionStr(direction) {
14863    if (direction == DIRECTION_DOWN) {
14864        return 'down';
14865    } else if (direction == DIRECTION_UP) {
14866        return 'up';
14867    } else if (direction == DIRECTION_LEFT) {
14868        return 'left';
14869    } else if (direction == DIRECTION_RIGHT) {
14870        return 'right';
14871    }
14872    return '';
14873}
14874
14875/**
14876 * get a recognizer by name if it is bound to a manager
14877 * @param {Recognizer|String} otherRecognizer
14878 * @param {Recognizer} recognizer
14879 * @returns {Recognizer}
14880 */
14881function getRecognizerByNameIfManager(otherRecognizer, recognizer) {
14882    var manager = recognizer.manager;
14883    if (manager) {
14884        return manager.get(otherRecognizer);
14885    }
14886    return otherRecognizer;
14887}
14888
14889/**
14890 * This recognizer is just used as a base for the simple attribute recognizers.
14891 * @constructor
14892 * @extends Recognizer
14893 */
14894function AttrRecognizer() {
14895    Recognizer.apply(this, arguments);
14896}
14897
14898inherit(AttrRecognizer, Recognizer, {
14899    /**
14900     * @namespace
14901     * @memberof AttrRecognizer
14902     */
14903    defaults: {
14904        /**
14905         * @type {Number}
14906         * @default 1
14907         */
14908        pointers: 1
14909    },
14910
14911    /**
14912     * Used to check if it the recognizer receives valid input, like input.distance > 10.
14913     * @memberof AttrRecognizer
14914     * @param {Object} input
14915     * @returns {Boolean} recognized
14916     */
14917    attrTest: function(input) {
14918        var optionPointers = this.options.pointers;
14919        return optionPointers === 0 || input.pointers.length === optionPointers;
14920    },
14921
14922    /**
14923     * Process the input and return the state for the recognizer
14924     * @memberof AttrRecognizer
14925     * @param {Object} input
14926     * @returns {*} State
14927     */
14928    process: function(input) {
14929        var state = this.state;
14930        var eventType = input.eventType;
14931
14932        var isRecognized = state & (STATE_BEGAN | STATE_CHANGED);
14933        var isValid = this.attrTest(input);
14934
14935        // on cancel input and we've recognized before, return STATE_CANCELLED
14936        if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) {
14937            return state | STATE_CANCELLED;
14938        } else if (isRecognized || isValid) {
14939            if (eventType & INPUT_END) {
14940                return state | STATE_ENDED;
14941            } else if (!(state & STATE_BEGAN)) {
14942                return STATE_BEGAN;
14943            }
14944            return state | STATE_CHANGED;
14945        }
14946        return STATE_FAILED;
14947    }
14948});
14949
14950/**
14951 * Pan
14952 * Recognized when the pointer is down and moved in the allowed direction.
14953 * @constructor
14954 * @extends AttrRecognizer
14955 */
14956function PanRecognizer() {
14957    AttrRecognizer.apply(this, arguments);
14958
14959    this.pX = null;
14960    this.pY = null;
14961}
14962
14963inherit(PanRecognizer, AttrRecognizer, {
14964    /**
14965     * @namespace
14966     * @memberof PanRecognizer
14967     */
14968    defaults: {
14969        event: 'pan',
14970        threshold: 10,
14971        pointers: 1,
14972        direction: DIRECTION_ALL
14973    },
14974
14975    getTouchAction: function() {
14976        var direction = this.options.direction;
14977        var actions = [];
14978        if (direction & DIRECTION_HORIZONTAL) {
14979            actions.push(TOUCH_ACTION_PAN_Y);
14980        }
14981        if (direction & DIRECTION_VERTICAL) {
14982            actions.push(TOUCH_ACTION_PAN_X);
14983        }
14984        return actions;
14985    },
14986
14987    directionTest: function(input) {
14988        var options = this.options;
14989        var hasMoved = true;
14990        var distance = input.distance;
14991        var direction = input.direction;
14992        var x = input.deltaX;
14993        var y = input.deltaY;
14994
14995        // lock to axis?
14996        if (!(direction & options.direction)) {
14997            if (options.direction & DIRECTION_HORIZONTAL) {
14998                direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT;
14999                hasMoved = x != this.pX;
15000                distance = Math.abs(input.deltaX);
15001            } else {
15002                direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN;
15003                hasMoved = y != this.pY;
15004                distance = Math.abs(input.deltaY);
15005            }
15006        }
15007        input.direction = direction;
15008        return hasMoved && distance > options.threshold && direction & options.direction;
15009    },
15010
15011    attrTest: function(input) {
15012        return AttrRecognizer.prototype.attrTest.call(this, input) &&
15013            (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input)));
15014    },
15015
15016    emit: function(input) {
15017
15018        this.pX = input.deltaX;
15019        this.pY = input.deltaY;
15020
15021        var direction = directionStr(input.direction);
15022
15023        if (direction) {
15024            input.additionalEvent = this.options.event + direction;
15025        }
15026        this._super.emit.call(this, input);
15027    }
15028});
15029
15030/**
15031 * Pinch
15032 * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out).
15033 * @constructor
15034 * @extends AttrRecognizer
15035 */
15036function PinchRecognizer() {
15037    AttrRecognizer.apply(this, arguments);
15038}
15039
15040inherit(PinchRecognizer, AttrRecognizer, {
15041    /**
15042     * @namespace
15043     * @memberof PinchRecognizer
15044     */
15045    defaults: {
15046        event: 'pinch',
15047        threshold: 0,
15048        pointers: 2
15049    },
15050
15051    getTouchAction: function() {
15052        return [TOUCH_ACTION_NONE];
15053    },
15054
15055    attrTest: function(input) {
15056        return this._super.attrTest.call(this, input) &&
15057            (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN);
15058    },
15059
15060    emit: function(input) {
15061        if (input.scale !== 1) {
15062            var inOut = input.scale < 1 ? 'in' : 'out';
15063            input.additionalEvent = this.options.event + inOut;
15064        }
15065        this._super.emit.call(this, input);
15066    }
15067});
15068
15069/**
15070 * Press
15071 * Recognized when the pointer is down for x ms without any movement.
15072 * @constructor
15073 * @extends Recognizer
15074 */
15075function PressRecognizer() {
15076    Recognizer.apply(this, arguments);
15077
15078    this._timer = null;
15079    this._input = null;
15080}
15081
15082inherit(PressRecognizer, Recognizer, {
15083    /**
15084     * @namespace
15085     * @memberof PressRecognizer
15086     */
15087    defaults: {
15088        event: 'press',
15089        pointers: 1,
15090        time: 251, // minimal time of the pointer to be pressed
15091        threshold: 9 // a minimal movement is ok, but keep it low
15092    },
15093
15094    getTouchAction: function() {
15095        return [TOUCH_ACTION_AUTO];
15096    },
15097
15098    process: function(input) {
15099        var options = this.options;
15100        var validPointers = input.pointers.length === options.pointers;
15101        var validMovement = input.distance < options.threshold;
15102        var validTime = input.deltaTime > options.time;
15103
15104        this._input = input;
15105
15106        // we only allow little movement
15107        // and we've reached an end event, so a tap is possible
15108        if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) {
15109            this.reset();
15110        } else if (input.eventType & INPUT_START) {
15111            this.reset();
15112            this._timer = setTimeoutContext(function() {
15113                this.state = STATE_RECOGNIZED;
15114                this.tryEmit();
15115            }, options.time, this);
15116        } else if (input.eventType & INPUT_END) {
15117            return STATE_RECOGNIZED;
15118        }
15119        return STATE_FAILED;
15120    },
15121
15122    reset: function() {
15123        clearTimeout(this._timer);
15124    },
15125
15126    emit: function(input) {
15127        if (this.state !== STATE_RECOGNIZED) {
15128            return;
15129        }
15130
15131        if (input && (input.eventType & INPUT_END)) {
15132            this.manager.emit(this.options.event + 'up', input);
15133        } else {
15134            this._input.timeStamp = now();
15135            this.manager.emit(this.options.event, this._input);
15136        }
15137    }
15138});
15139
15140/**
15141 * Rotate
15142 * Recognized when two or more pointer are moving in a circular motion.
15143 * @constructor
15144 * @extends AttrRecognizer
15145 */
15146function RotateRecognizer() {
15147    AttrRecognizer.apply(this, arguments);
15148}
15149
15150inherit(RotateRecognizer, AttrRecognizer, {
15151    /**
15152     * @namespace
15153     * @memberof RotateRecognizer
15154     */
15155    defaults: {
15156        event: 'rotate',
15157        threshold: 0,
15158        pointers: 2
15159    },
15160
15161    getTouchAction: function() {
15162        return [TOUCH_ACTION_NONE];
15163    },
15164
15165    attrTest: function(input) {
15166        return this._super.attrTest.call(this, input) &&
15167            (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN);
15168    }
15169});
15170
15171/**
15172 * Swipe
15173 * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction.
15174 * @constructor
15175 * @extends AttrRecognizer
15176 */
15177function SwipeRecognizer() {
15178    AttrRecognizer.apply(this, arguments);
15179}
15180
15181inherit(SwipeRecognizer, AttrRecognizer, {
15182    /**
15183     * @namespace
15184     * @memberof SwipeRecognizer
15185     */
15186    defaults: {
15187        event: 'swipe',
15188        threshold: 10,
15189        velocity: 0.3,
15190        direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL,
15191        pointers: 1
15192    },
15193
15194    getTouchAction: function() {
15195        return PanRecognizer.prototype.getTouchAction.call(this);
15196    },
15197
15198    attrTest: function(input) {
15199        var direction = this.options.direction;
15200        var velocity;
15201
15202        if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) {
15203            velocity = input.overallVelocity;
15204        } else if (direction & DIRECTION_HORIZONTAL) {
15205            velocity = input.overallVelocityX;
15206        } else if (direction & DIRECTION_VERTICAL) {
15207            velocity = input.overallVelocityY;
15208        }
15209
15210        return this._super.attrTest.call(this, input) &&
15211            direction & input.offsetDirection &&
15212            input.distance > this.options.threshold &&
15213            input.maxPointers == this.options.pointers &&
15214            abs(velocity) > this.options.velocity && input.eventType & INPUT_END;
15215    },
15216
15217    emit: function(input) {
15218        var direction = directionStr(input.offsetDirection);
15219        if (direction) {
15220            this.manager.emit(this.options.event + direction, input);
15221        }
15222
15223        this.manager.emit(this.options.event, input);
15224    }
15225});
15226
15227/**
15228 * A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur
15229 * between the given interval and position. The delay option can be used to recognize multi-taps without firing
15230 * a single tap.
15231 *
15232 * The eventData from the emitted event contains the property `tapCount`, which contains the amount of
15233 * multi-taps being recognized.
15234 * @constructor
15235 * @extends Recognizer
15236 */
15237function TapRecognizer() {
15238    Recognizer.apply(this, arguments);
15239
15240    // previous time and center,
15241    // used for tap counting
15242    this.pTime = false;
15243    this.pCenter = false;
15244
15245    this._timer = null;
15246    this._input = null;
15247    this.count = 0;
15248}
15249
15250inherit(TapRecognizer, Recognizer, {
15251    /**
15252     * @namespace
15253     * @memberof PinchRecognizer
15254     */
15255    defaults: {
15256        event: 'tap',
15257        pointers: 1,
15258        taps: 1,
15259        interval: 300, // max time between the multi-tap taps
15260        time: 250, // max time of the pointer to be down (like finger on the screen)
15261        threshold: 9, // a minimal movement is ok, but keep it low
15262        posThreshold: 10 // a multi-tap can be a bit off the initial position
15263    },
15264
15265    getTouchAction: function() {
15266        return [TOUCH_ACTION_MANIPULATION];
15267    },
15268
15269    process: function(input) {
15270        var options = this.options;
15271
15272        var validPointers = input.pointers.length === options.pointers;
15273        var validMovement = input.distance < options.threshold;
15274        var validTouchTime = input.deltaTime < options.time;
15275
15276        this.reset();
15277
15278        if ((input.eventType & INPUT_START) && (this.count === 0)) {
15279            return this.failTimeout();
15280        }
15281
15282        // we only allow little movement
15283        // and we've reached an end event, so a tap is possible
15284        if (validMovement && validTouchTime && validPointers) {
15285            if (input.eventType != INPUT_END) {
15286                return this.failTimeout();
15287            }
15288
15289            var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true;
15290            var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold;
15291
15292            this.pTime = input.timeStamp;
15293            this.pCenter = input.center;
15294
15295            if (!validMultiTap || !validInterval) {
15296                this.count = 1;
15297            } else {
15298                this.count += 1;
15299            }
15300
15301            this._input = input;
15302
15303            // if tap count matches we have recognized it,
15304            // else it has began recognizing...
15305            var tapCount = this.count % options.taps;
15306            if (tapCount === 0) {
15307                // no failing requirements, immediately trigger the tap event
15308                // or wait as long as the multitap interval to trigger
15309                if (!this.hasRequireFailures()) {
15310                    return STATE_RECOGNIZED;
15311                } else {
15312                    this._timer = setTimeoutContext(function() {
15313                        this.state = STATE_RECOGNIZED;
15314                        this.tryEmit();
15315                    }, options.interval, this);
15316                    return STATE_BEGAN;
15317                }
15318            }
15319        }
15320        return STATE_FAILED;
15321    },
15322
15323    failTimeout: function() {
15324        this._timer = setTimeoutContext(function() {
15325            this.state = STATE_FAILED;
15326        }, this.options.interval, this);
15327        return STATE_FAILED;
15328    },
15329
15330    reset: function() {
15331        clearTimeout(this._timer);
15332    },
15333
15334    emit: function() {
15335        if (this.state == STATE_RECOGNIZED) {
15336            this._input.tapCount = this.count;
15337            this.manager.emit(this.options.event, this._input);
15338        }
15339    }
15340});
15341
15342/**
15343 * Simple way to create a manager with a default set of recognizers.
15344 * @param {HTMLElement} element
15345 * @param {Object} [options]
15346 * @constructor
15347 */
15348function 
vendor: 8,263 bytes, lines 15348-15610
15348NGHammer(element, options) {
15349    options = options || {};
15350    options.recognizers = ifUndefined(options.recognizers, NGHammer.defaults.preset);
15351    return new Manager(element, options);
15352}
15353
15354/**
15355 * @const {string}
15356 */
15357NGHammer.VERSION = '2.0.7';
15358
15359/**
15360 * default settings
15361 * @namespace
15362 */
15363NGHammer.defaults = {
15364    /**
15365     * set if DOM events are being triggered.
15366     * But this is slower and unused by simple implementations, so disabled by default.
15367     * @type {Boolean}
15368     * @default false
15369     */
15370    domEvents: false,
15371
15372    /**
15373     * The value for the touchAction property/fallback.
15374     * When set to `compute` it will magically set the correct value based on the added recognizers.
15375     * @type {String}
15376     * @default compute
15377     */
15378    touchAction: TOUCH_ACTION_COMPUTE,
15379
15380    /**
15381     * @type {Boolean}
15382     * @default true
15383     */
15384    enable: true,
15385
15386    /**
15387     * EXPERIMENTAL FEATURE -- can be removed/changed
15388     * Change the parent input target element.
15389     * If Null, then it is being set the to main element.
15390     * @type {Null|EventTarget}
15391     * @default null
15392     */
15393    inputTarget: null,
15394
15395    /**
15396     * force an input class
15397     * @type {Null|Function}
15398     * @default null
15399     */
15400    inputClass: null,
15401
15402    /**
15403     * Default recognizer setup when calling `NGHammer()`
15404     * When creating a new Manager these will be skipped.
15405     * @type {Array}
15406     */
15407    preset: [
15408        // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...]
15409        [RotateRecognizer, {enable: false}],
15410        [PinchRecognizer, {enable: false}, ['rotate']],
15411        [SwipeRecognizer, {direction: DIRECTION_HORIZONTAL}],
15412        [PanRecognizer, {direction: DIRECTION_HORIZONTAL}, ['swipe']],
15413        [TapRecognizer],
15414        [TapRecognizer, {event: 'doubletap', taps: 2}, ['tap']],
15415        [PressRecognizer]
15416    ],
15417
15418    /**
15419     * Some CSS properties can be used to improve the working of NGHammer.
15420     * Add them to this method and they will be set when creating a new Manager.
15421     * @namespace
15422     */
15423    cssProps: {
15424        /**
15425         * Disables text selection to improve the dragging gesture. Mainly for desktop browsers.
15426         * @type {String}
15427         * @default 'none'
15428         */
15429        userSelect: 'none',
15430
15431        /**
15432         * Disable the Windows Phone grippers when pressing an element.
15433         * @type {String}
15434         * @default 'none'
15435         */
15436        touchSelect: 'none',
15437
15438        /**
15439         * Disables the default callout shown when you touch and hold a touch target.
15440         * On iOS, when you touch and hold a touch target such as a link, Safari displays
15441         * a callout containing information about the link. This property allows you to disable that callout.
15442         * @type {String}
15443         * @default 'none'
15444         */
15445        touchCallout: 'none',
15446
15447        /**
15448         * Specifies whether zooming is enabled. Used by IE10>
15449         * @type {String}
15450         * @default 'none'
15451         */
15452        contentZooming: 'none',
15453
15454        /**
15455         * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers.
15456         * @type {String}
15457         * @default 'none'
15458         */
15459        userDrag: 'none',
15460
15461        /**
15462         * Overrides the highlight color shown when the user taps a link or a JavaScript
15463         * clickable element in iOS. This property obeys the alpha value, if specified.
15464         * @type {String}
15465         * @default 'rgba(0,0,0,0)'
15466         */
15467        tapHighlightColor: 'rgba(0,0,0,0)'
15468    }
15469};
15470
15471var STOP = 1;
15472var FORCED_STOP = 2;
15473
15474/**
15475 * Manager
15476 * @param {HTMLElement} element
15477 * @param {Object} [options]
15478 * @constructor
15479 */
15480function Manager(element, options) {
15481    this.options = assign({}, NGHammer.defaults, options || {});
15482
15483    this.options.inputTarget = this.options.inputTarget || element;
15484
15485    this.handlers = {};
15486    this.session = {};
15487    this.recognizers = [];
15488    this.oldCssProps = {};
15489
15490    this.element = element;
15491    this.input = createInputInstance(this);
15492    this.touchAction = new TouchAction(this, this.options.touchAction);
15493
15494    toggleCssProps(this, true);
15495
15496    each(this.options.recognizers, function(item) {
15497        var recognizer = this.add(new (item[0])(item[1]));
15498        item[2] && recognizer.recognizeWith(item[2]);
15499        item[3] && recognizer.requireFailure(item[3]);
15500    }, this);
15501}
15502
15503Manager.prototype = {
15504    /**
15505     * set options
15506     * @param {Object} options
15507     * @returns {Manager}
15508     */
15509    set: function(options) {
15510        assign(this.options, options);
15511
15512        // Options that need a little more setup
15513        if (options.touchAction) {
15514            this.touchAction.update();
15515        }
15516        if (options.inputTarget) {
15517            // Clean up existing event listeners and reinitialize
15518            this.input.destroy();
15519            this.input.target = options.inputTarget;
15520            this.input.init();
15521        }
15522        return this;
15523    },
15524
15525    /**
15526     * stop recognizing for this session.
15527     * This session will be discarded, when a new [input]start event is fired.
15528     * When forced, the recognizer cycle is stopped immediately.
15529     * @param {Boolean} [force]
15530     */
15531    stop: function(force) {
15532        this.session.stopped = force ? FORCED_STOP : STOP;
15533    },
15534
15535    /**
15536     * run the recognizers!
15537     * called by the inputHandler function on every movement of the pointers (touches)
15538     * it walks through all the recognizers and tries to detect the gesture that is being made
15539     * @param {Object} inputData
15540     */
15541    recognize: function(inputData) {
15542        var session = this.session;
15543        if (session.stopped) {
15544            return;
15545        }
15546
15547        // run the touch-action polyfill
15548        this.touchAction.preventDefaults(inputData);
15549
15550        var recognizer;
15551        var recognizers = this.recognizers;
15552
15553        // this holds the recognizer that is being recognized.
15554        // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED
15555        // if no recognizer is detecting a thing, it is set to `null`
15556        var curRecognizer = session.curRecognizer;
15557
15558        // reset when the last recognizer is recognized
15559        // or when we're in a new session
15560        if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) {
15561            curRecognizer = session.curRecognizer = null;
15562        }
15563
15564        var i = 0;
15565        while (i < recognizers.length) {
15566            recognizer = recognizers[i];
15567
15568            // find out if we are allowed try to recognize the input for this one.
15569            // 1.   allow if the session is NOT forced stopped (see the .stop() method)
15570            // 2.   allow if we still haven't recognized a gesture in this session, or the this recognizer is the one
15571            //      that is being recognized.
15572            // 3.   allow if the recognizer is allowed to run simultaneous with the current recognized recognizer.
15573            //      this can be setup with the `recognizeWith()` method on the recognizer.
15574            if (session.stopped !== FORCED_STOP && ( // 1
15575                    !curRecognizer || recognizer == curRecognizer || // 2
15576                    recognizer.canRecognizeWith(curRecognizer))) { // 3
15577                recognizer.recognize(inputData);
15578            } else {
15579                recognizer.reset();
15580            }
15581
15582            // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the
15583            // current active recognizer. but only if we don't already have an active recognizer
15584            if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) {
15585                curRecognizer = session.curRecognizer = recognizer;
15586            }
15587            i++;
15588        }
15589    },
15590
15591    /**
15592     * get a recognizer by its event name.
15593     * @param {Recognizer|String} recognizer
15594     * @returns {Recognizer|Null}
15595     */
15596    get: function(recognizer) {
15597        if (recognizer instanceof Recognizer) {
15598            return recognizer;
15599        }
15600
15601        var recognizers = this.recognizers;
15602        for (var i = 0; i < recognizers.length; i++) {
15603            if (recognizers[i].options.event == recognizer) {
15604                return recognizers[i];
15605            }
15606        }
15607        return null;
15608    },
15609
15610    /**
vendor: 9,974 bytes, lines 15611-15964
15611     * add a recognizer to the manager
15612     * existing recognizers with the same event name will be removed
15613     * @param {Recognizer} recognizer
15614     * @returns {Recognizer|Manager}
15615     */
15616    add: function(recognizer) {
15617        if (invokeArrayArg(recognizer, 'add', this)) {
15618            return this;
15619        }
15620
15621        // remove existing
15622        var existing = this.get(recognizer.options.event);
15623        if (existing) {
15624            this.remove(existing);
15625        }
15626
15627        this.recognizers.push(recognizer);
15628        recognizer.manager = this;
15629
15630        this.touchAction.update();
15631        return recognizer;
15632    },
15633
15634    /**
15635     * remove a recognizer by name or instance
15636     * @param {Recognizer|String} recognizer
15637     * @returns {Manager}
15638     */
15639    remove: function(recognizer) {
15640        if (invokeArrayArg(recognizer, 'remove', this)) {
15641            return this;
15642        }
15643
15644        recognizer = this.get(recognizer);
15645
15646        // let's make sure this recognizer exists
15647        if (recognizer) {
15648            var recognizers = this.recognizers;
15649            var index = inArray(recognizers, recognizer);
15650
15651            if (index !== -1) {
15652                recognizers.splice(index, 1);
15653                this.touchAction.update();
15654            }
15655        }
15656
15657        return this;
15658    },
15659
15660    /**
15661     * bind event
15662     * @param {String} events
15663     * @param {Function} handler
15664     * @returns {EventEmitter} this
15665     */
15666    on: function(events, handler) {
15667        if (events === undefined) {
15668            return;
15669        }
15670        if (handler === undefined) {
15671            return;
15672        }
15673
15674        var handlers = this.handlers;
15675        each(splitStr(events), function(event) {
15676            handlers[event] = handlers[event] || [];
15677            handlers[event].push(handler);
15678        });
15679        return this;
15680    },
15681
15682    /**
15683     * unbind event, leave emit blank to remove all handlers
15684     * @param {String} events
15685     * @param {Function} [handler]
15686     * @returns {EventEmitter} this
15687     */
15688    off: function(events, handler) {
15689        if (events === undefined) {
15690            return;
15691        }
15692
15693        var handlers = this.handlers;
15694        each(splitStr(events), function(event) {
15695            if (!handler) {
15696                delete handlers[event];
15697            } else {
15698                handlers[event] && handlers[event].splice(inArray(handlers[event], handler), 1);
15699            }
15700        });
15701        return this;
15702    },
15703
15704    /**
15705     * emit event to the listeners
15706     * @param {String} event
15707     * @param {Object} data
15708     */
15709    emit: function(event, data) {
15710        // we also want to trigger dom events
15711        if (this.options.domEvents) {
15712            triggerDomEvent(event, data);
15713        }
15714
15715        // no handlers, so skip it all
15716        var handlers = this.handlers[event] && this.handlers[event].slice();
15717        if (!handlers || !handlers.length) {
15718            return;
15719        }
15720
15721        data.type = event;
15722        data.preventDefault = function() {
15723            data.srcEvent.preventDefault();
15724        };
15725
15726        var i = 0;
15727        while (i < handlers.length) {
15728            handlers[i](data);
15729            i++;
15730        }
15731    },
15732
15733    /**
15734     * destroy the manager and unbinds all events
15735     * it doesn't unbind dom events, that is the user own responsibility
15736     */
15737    destroy: function() {
15738        this.element && toggleCssProps(this, false);
15739
15740        this.handlers = {};
15741        this.session = {};
15742        this.input.destroy();
15743        this.element = null;
15744    }
15745};
15746
15747/**
15748 * add/remove the css properties as defined in manager.options.cssProps
15749 * @param {Manager} manager
15750 * @param {Boolean} add
15751 */
15752function toggleCssProps(manager, add) {
15753    var element = manager.element;
15754    if (!element.style) {
15755        return;
15756    }
15757    var prop;
15758    each(manager.options.cssProps, function(value, name) {
15759        prop = prefixed(element.style, name);
15760        if (add) {
15761            manager.oldCssProps[prop] = element.style[prop];
15762            element.style[prop] = value;
15763        } else {
15764            element.style[prop] = manager.oldCssProps[prop] || '';
15765        }
15766    });
15767    if (!add) {
15768        manager.oldCssProps = {};
15769    }
15770}
15771
15772/**
15773 * trigger dom event
15774 * @param {String} event
15775 * @param {Object} data
15776 */
15777function triggerDomEvent(event, data) {
15778    var gestureEvent = document.createEvent('Event');
15779    gestureEvent.initEvent(event, true, true);
15780    gestureEvent.gesture = data;
15781    data.target.dispatchEvent(gestureEvent);
15782}
15783
15784assign(NGHammer, {
15785    INPUT_START: INPUT_START,
15786    INPUT_MOVE: INPUT_MOVE,
15787    INPUT_END: INPUT_END,
15788    INPUT_CANCEL: INPUT_CANCEL,
15789
15790    STATE_POSSIBLE: STATE_POSSIBLE,
15791    STATE_BEGAN: STATE_BEGAN,
15792    STATE_CHANGED: STATE_CHANGED,
15793    STATE_ENDED: STATE_ENDED,
15794    STATE_RECOGNIZED: STATE_RECOGNIZED,
15795    STATE_CANCELLED: STATE_CANCELLED,
15796    STATE_FAILED: STATE_FAILED,
15797
15798    DIRECTION_NONE: DIRECTION_NONE,
15799    DIRECTION_LEFT: DIRECTION_LEFT,
15800    DIRECTION_RIGHT: DIRECTION_RIGHT,
15801    DIRECTION_UP: DIRECTION_UP,
15802    DIRECTION_DOWN: DIRECTION_DOWN,
15803    DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL,
15804    DIRECTION_VERTICAL: DIRECTION_VERTICAL,
15805    DIRECTION_ALL: DIRECTION_ALL,
15806
15807    Manager: Manager,
15808    Input: Input,
15809    TouchAction: TouchAction,
15810
15811    TouchInput: TouchInput,
15812    MouseInput: MouseInput,
15813    PointerEventInput: PointerEventInput,
15814    TouchMouseInput: TouchMouseInput,
15815    SingleTouchInput: SingleTouchInput,
15816
15817    Recognizer: Recognizer,
15818    AttrRecognizer: AttrRecognizer,
15819    Tap: TapRecognizer,
15820    Pan: PanRecognizer,
15821    Swipe: SwipeRecognizer,
15822    Pinch: PinchRecognizer,
15823    Rotate: RotateRecognizer,
15824    Press: PressRecognizer,
15825
15826    on: addEventListeners,
15827    off: removeEventListeners,
15828    each: each,
15829    merge: merge,
15830    extend: extend,
15831    assign: assign,
15832    inherit: inherit,
15833    bindFn: bindFn,
15834    prefixed: prefixed
15835});
15836
15837// this prevents errors when NGHammer is loaded in the presence of an AMD
15838//  style loader but by script tag, not by the loader.
15839var freeGlobal = (typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : {})); // jshint ignore:line
15840freeGlobal.NGHammer = NGHammer;
15841
15842if (typeof define === 'function' && define.amdDISABLED) {
15843    define(function() {
15844        return NGHammer;
15845    });
15846} else if (typeof module != 'undefined' && module.exports) {
15847    module.exports = NGHammer;
15848} else {
15849    window[exportName] = NGHammer;
15850}
15851
15852})(window, document, 'NGHammer');
15853
15854
15855
15856
15857
15858
15859// END NANOGALLERY2
15860// }( jQuery )));
15861}));
15862
15863
15864//##########################################################################################################################
15865//##########################################################################################################################
15866//##########################################################################################################################
15867//##########################################################################################################################
15868//##########################################################################################################################
15869
15870// nanogallery2 auto start whithout javascript call
15871(function(){
15872  'use strict';
15873	
15874	function document_ready(callback){
15875		// in case the document is already rendered
15876		if (document.readyState!='loading') callback();
15877		// modern browsers
15878		else if (document.addEventListener) document.addEventListener('DOMContentLoaded', callback);
15879		// IE <= 8
15880		else document.attachEvent('onreadystatechange', function(){
15881				if (document.readyState=='complete') callback();
15882		});
15883	}
15884
15885	document_ready(function(){
15886		
15887		// retrieve GALLERIES
15888		var t=document.querySelectorAll('[data-nanogallery2]');
15889		for( var i=0; i < t.length; i++ ) {
15890			jQuery( t[i] ).nanogallery2( jQuery(t[i]).data('nanogallery2') );
15891		}
15892		
15893		// retrieve SINGLE ELEMENTS -> ONLY LIGHTBOX / NO GALLERY
15894		var t = document.querySelectorAll('[data-nanogallery2-lightbox]');
15895		for( var i=0; i < t.length; i++ ) {
15896			
15897      // set mouse pointer
15898      t[i].classList.add('NGY2ThumbnailLightbox');
15899
15900      // add click event
15901      t[i].addEventListener('click', function(e) {
15902        // disable link tag if <A> element
15903        e.preventDefault();
15904      
15905        // default options for standalone lightbox
15906        var options = { 
15907          lightboxStandalone: true,
15908          viewerToolbar: { display: false }
15909        };
15910
15911        // group of images
15912        var g = this.dataset.nanogallery2Lgroup;
15913
15914        // Retrieve the lightbox configuration
15915        // it just need to be defined on one of the elements, which will be displayed in the lightbox
15916        var t = document.querySelectorAll('[data-nanogallery2-lightbox]');
15917        for( var i=0; i < t.length; i++ ) {
15918          if( t[i].dataset.nanogallery2Lgroup == g ) {
15919            if( t[i].dataset.nanogallery2Lightbox !== "" ) {
15920              options = jQuery.extend(true, {}, options, jQuery(t[i]).data('nanogallery2Lightbox'));
15921              break;
15922            }
15923          }
15924        }
15925        jQuery( this ).nanogallery2( options );
15926
15927      });
15928
15929		}
15930	});
15931	
15932	
15933	
15934  // jQuery(document).ready(function () {
15935  
15936    // var t=document.querySelectorAll('[data-nanogallery2-portable]');
15937    // if( t.length > 0 ) {
15938      // portable mode
15939      // var link = document.createElement('link');
15940      // link.setAttribute("rel", "stylesheet");
15941      // link.setAttribute("type", "text/css");
15942      // link.onload = function(){
15943        // for( var i=0; i < t.length; i++ ) {
15944          // jQuery(t[i]).nanogallery2(jQuery(t[i]).data('nanogallery2-portable'));
15945        // }
15946      // }
15947      // link.setAttribute("href", '//nano.gallery/css/nanogallery2.css');
15948      // document.getElementsByTagName("head")[0].appendChild(link);
15949    // }
15950    // else {
15951      // standard mode
15952			
15953			// GALLERIES
15954      // var t=document.querySelectorAll('[data-nanogallery2]');
15955      // for( var i=0; i < t.length; i++ ) {
15956        // jQuery( t[i] ).nanogallery2( jQuery(t[i]).data('nanogallery2') );
15957      // }
15958			
15959		
15960    // }
15961    
15962  // });
15963}).call(null);
15964

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.