PageSourceSearch

https://desales.edu/_files/js/jquery.nanogallery2.js

js desales.edu collected 2026-09-24 09:45:20 UTC 585,786 bytes, 15,949 lines download raw bytes

vendor: 37,254 bytes, lines 1-874
1/* nanogallery2 - v2.4.2 - 2019-03-22 - https://nanogallery2.nanostudio.org */
2/**!
3 * @preserve nanogallery2 - javascript photo / video gallery and lightbox
4 * Homepage: http://nanogallery2.nanostudio.org
5 * Sources:  https://github.com/nanostudio-org/nanogallery2
6 *
7 * License:  GPLv3 and commercial licence
8 * 
9 * Requirements:
10 *  - jQuery (http://www.jquery.com) - version >= 1.7.1
11 *
12 * Embeded components:
13 *  - shifty (https://github.com/jeremyckahn/shifty)
14 *  - imagesloaded (https://github.com/desandro/imagesloaded)
15 *  - hammer.js (http://hammerjs.github.io/)
16 *  - screenfull.js (https://github.com/sindresorhus/screenfull.js)
17 * Tools:
18 *  - webfont generated with http://fontello.com - mainly based on Font Awesome Copyright (C) 2012 by Dave Gandy (http://fontawesome.io/)
19 *  - ICO online converter: https://iconverticons.com/online/
20 */
21
22 
23 
24// ###########################################
25// ##### nanogallery2 as a JQUERY PLUGIN #####
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  // avoid if possible (performance issue)
133  function inViewport( $elt, threshold ) {
134    var wp = getViewport(),
135    eltOS = $elt.offset(),
136    th = $elt.outerHeight(true),
137    tw = $elt.outerWidth(true);
138    if( eltOS.top >= (wp.t - threshold) 
139      && (eltOS.top + th) <= (wp.t + wp.h + threshold)
140      && eltOS.left >= (wp.l - threshold) 
141      && (eltOS.left + tw) <= (wp.l + wp.w + threshold) ) {
142      return true;
143    }
144    else {
145      return false;
146    }
147  }
148
149  // avoid if possible (performance issue)
150  function inViewportVert( $elt, threshold ) {
151    var wp = getViewport(),
152    eltOS = $elt.offset(),
153    th = $elt.outerHeight(true);
154    //var tw=$elt.outerWidth(true);
155
156    if( wp.t == 0 && (eltOS.top) <= (wp.t + wp.h ) ) { return true; }
157
158    if( eltOS.top >= (wp.t) 
159      && (eltOS.top + th) <= (wp.t + wp.h - threshold) ) {
160        return true;
161    }
162    else {
163      return false;
164    }
165  }
166
167
168  // set z-index to display 2 elements on top of all others
169  function set2ElementsOnTop( start, elt1, elt2 ) {
170    var highest_index = 0;
171    if( start=='' ) { start= '*'; }
172    jQuery(start).each(function() {
173      var cur = parseInt(jQuery(this).css('z-index'));
174      highest_index = cur > highest_index ? cur : highest_index;
175    });
176    highest_index++;
177    jQuery(elt2).css('z-index',highest_index+1);
178    jQuery(elt1).css('z-index',highest_index);
179  }
180
181  // set z-index to display element on top of all others
182  function setElementOnTop( start, elt ) {
183    var highest_index = 0;
184    if( start == '' ) { start = '*'; }
185    jQuery(start).each(function() {
186      var cur = parseInt(jQuery(this).css('z-index'));
187      highest_index = cur > highest_index ? cur : highest_index;
188    });
189    highest_index++;
190    jQuery(elt).css('z-index',highest_index);
191  }
192  
193  // return the real type of the object
194  var toType = function( obj ) {
195    // by Angus Croll - http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/
196    return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase()
197  };    
198    
199
200  $.nanogallery2 = function (elt, options) {
201    // To avoid scope issues, use '_this' instead of 'this'
202    // to reference this class from internal events and functions.
203    var _this = this;
204
205    // Access to jQuery and DOM versions of element
206    _this.$e  = jQuery(elt);
207    _this.e   = elt;
208
209    // Add a reverse reference to the DOM object
210    _this.$e.data('nanogallery2data', _this);
211
212    _this.init = function () {
213  
214      // define these global objects only once per HTML page
215      if (typeof window.NGY2Item === 'undefined') {
216  
217        window.NGY2Tools = (function () {
218
219          function NGY2Tools() {
220            var nextId = 1;                   // private static --> all instances
221          }
222
223          // check album name - albumList/blackList/whiteList
224          NGY2Tools.FilterAlbumName = function( title, ID ) {
225            var s = title.toUpperCase();
226            if( this.albumList.length > 0 ) {
227              for( var j=0; j < this.albumList.length; j++) {
228                if( s === this.albumList[j].toUpperCase() || ID === this.albumList[j] ) {
229                  return true;
230                }
231              }
232            }
233            else {
234              var found = false;
235              if( this.whiteList !== null ) {
236                //whiteList : authorize only album cointaining one of the specified keyword in the title
237                for( var j = 0; j < this.whiteList.length; j++) {
238                  if( s.indexOf(this.whiteList[j]) !== -1 ) {
239                    found = true;
240                  }
241                }
242                if( !found ) { return false; }
243              }
244
245
246              if( this.blackList !== null ) {
247                //blackList : ignore album cointaining one of the specified keyword in the title
248                for( var j = 0; j < this.blackList.length; j++) {
249                  if( s.indexOf(this.blackList[j]) !== -1 ) { 
250                    return false;
251                  }
252                }
253              }
254              return true;
255            }
256          };
257
258
259          /** @function nanoAlert */
260          /* Display an alert message in a specific element */
261          NGY2Tools.NanoAlert = function(context, msg, verbose) {
262            NGY2Tools.NanoConsoleLog.call(context, msg);
263            if( context.$E.conConsole != null ) {
264              context.$E.conConsole.css({visibility: 'visible', minHeight: '100px'});
265              if( verbose == false ) {
266                context.$E.conConsole.append('<p>' + msg + '</p>');
267              }
268              else {
269                context.$E.conConsole.append('<p>nanogallery2: '+ msg + ' [' + context.baseEltID + ']</p>');
270              }
271            }
272          };
273          
274  
275          /** @function NanoConsoleLog */
276          /* write message to the browser console */
277          NGY2Tools.NanoConsoleLog = function(context, msg) {
278            if (window.console) { console.log('nanogallery2: ' + msg + ' [' + context.baseEltID + ']'); }
279          };
280          
281
282          /** @function PreloaderDisplay() */
283          /* Display/hide preloader */
284          NGY2Tools.PreloaderDisplay = function(display) {
285            if( display === true ) {
286              this.$E.conLoadingB.removeClass('nanoGalleryLBarOff').addClass('nanoGalleryLBar');
287            }
288            else {
289              this.$E.conLoadingB.removeClass('nanoGalleryLBar').addClass('nanoGalleryLBarOff');
290            }
291          };
292
293          //+ Jonas Raoni Soares Silva
294          //@ http://jsfromhell.com/array/shuffle [v1.0]
295          NGY2Tools.AreaShuffle = function (o) {
296            for (var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
297            return o;
298          };
299          
300          /** @function GetImageTitleFromURL() */
301          /* retrieve filemane */
302          NGY2Tools.GetImageTitleFromURL = function( imageURL ) {
303            if( this.O.thumbnailLabel.get('title') == '%filename' ) {
304              return (imageURL.split('/').pop()).replace('_',' ');
305            }
306            
307            if( this.O.thumbnailLabel.get('title') == '%filenameNoExt' ) {
308              var s=imageURL.split('/').pop();
309              return (s.split('.').shift()).replace('_',' ');
310            }
311            // return imageURL;
312            return '';
313          };
314          
315
316          /** @function AlbumPostProcess() */
317          /* post process one album based on plugin general parameters  --> sorting/maxItems*/
318          NGY2Tools.AlbumPostProcess = function(albumID) {
319
320            // this function can probably be optimized....
321          
322            var sortOrder = this.gallerySorting[this.GOM.curNavLevel];
323            var maxItems = this.galleryMaxItems[this.GOM.curNavLevel];
324          
325            if( sortOrder != '' || maxItems > 0 ) {
326            
327              // copy album's items to a new array
328              var currentAlbum = this.I.filter( function( obj ) {
329                return( obj.albumID == albumID && obj.kind != 'albumUp' );
330              });
331        
332              // sorting options
333              switch( sortOrder ) {
334                case 'RANDOM':
335                  currentAlbum = NGY2Tools.AreaShuffle(currentAlbum);
336                  break;
337                case 'REVERSED':
338                  currentAlbum = currentAlbum.reverse();
339                  break;
340                case 'TITLEASC':
341                  currentAlbum.sort(function (a, b) {
342                    return( (a.title.toUpperCase() < b.title.toUpperCase()) ? -1 : ((a.title.toUpperCase() > b.title.toUpperCase()) ? 1 : 0) );
343                  });
344                  break;
345                case 'TITLEDESC':
346                  currentAlbum.sort(function (a, b) {
347                    return( (a.title.toUpperCase() > b.title.toUpperCase()) ? -1 : ((a.title.toUpperCase() < b.title.toUpperCase()) ? 1 : 0) );
348                  });
349                  break;
350              }     
351
352              // max Items
353              if( maxItems > 0 && currentAlbum.length > maxItems ) {
354                currentAlbum.splice(maxItems - 1, currentAlbum.length-maxItems );
355              }
356              
357              // remove the albums's items from the global items array
358              this.I.removeIf( function( obj ) {
359                return( obj.albumID == albumID && obj.kind != 'albumUp' );
360              });
361              
362              // add the sorted items back to the album
363              this.I.push.apply(this.I, currentAlbum);
364
365            }
366          };
367          
368          return NGY2Tools;
369        })(); 
370
371        // ====================
372        // ===== NGY2Item =====
373        // ====================
374        window.NGY2Item = (function() {
375          var nextId = 1;                   // private static --> all instances
376
377          // constructor
378          function NGY2Item( itemID ) {
379          //window.NGY2Item = function( itemID ) {
380            var ID = 0;                     // private
381
382            // public (this instance only)
383            if( itemID === undefined || itemID === null ) {
384              ID = nextId++;
385            }
386            else {
387              ID = itemID;
388            }
389            this.GetID = function () { return ID; };
390            
391            // public
392            this.kind =                 '';       // 'image', 'album' or 'albumUp'
393            this.mediaKind =            'img';    // 'img', 'iframe'
394            this.mediaMarkup =          '';
395            this.G =                    null;     // pointer to global instance
396            this.title =                '';       // image title
397            this.description =          '';       // image description
398            this.albumID =              0;        // ID of the parent album
399            this.src =                  '';       // full sized image URL
400            this.width =                0;        // image width
401            this.height =               0;        // image height
402            this.destinationURL =       '';       // thumbnail destination URL --> open URL instead of displaying image
403            this.downloadURL =          '';       // thumbnail download URL --> specify the image for download button
404            this.author =               '';       // image/album author
405            this.left=                  0;        // store position to animate from old to new
406            this.top=                   0;
407            this.width=                 0;        // store size to avoid setting width/height if not required
408            this.height=                0;
409            this.resizedContentWidth=   0;        // store size of content (image) to avoid setting width/height if not required
410            this.resizedContentHeight=  0;
411            this.thumbs = {                       // URLs and sizes for user defined
412              url:    { l1: { xs: '', sm:'', me: '', la: '', xl: '' }, lN: { xs: '', sm: '', me: '', la:'', xl: '' } },
413              width:  { l1: { xs: 0,  sm: 0, me: 0,  la: 0 , xl: 0  }, lN: { xs: 0 , sm: 0,  me: 0,  la: 0, xl: 0  } },
414              height: { l1: { xs: 0,  sm: 0, me: 0,  la: 0 , xl: 0  }, lN: { xs: 0,  sm: 0,  me: 0,  la: 0, xl: 0  } }
415            };
416            this.thumbnailImgRevealed   = false;  // thumbnail image already revealed
417            this.imageDominantColors    = null;   // base64 GIF
418            this.imageDominantColor     = null;   // HEX RGB
419            this.featured =             false;    // featured element
420            this.flickrThumbSizes =     {};       // store URLs for all available thumbnail sizes (flickr)
421            this.picasaThumbs =         null;     // store URLs and sizes
422            this.hovered =              false;    // is the thumbnail currently hovered?
423            this.hoverInitDone =        false;
424            this.contentIsLoaded =      false;    // album: are items already loaded?
425            this.contentLength =        0;        // album: number of items (real number of items in memory)
426            this.numberItems =          0;        // album: number of items (value returned by data source)
427            this.mediaNumber =          0;        // media number in the album
428            this.imageCounter =         0;        // number of images in an album
429            this.eltTransform =         [];       // store the CSS transformations
430            this.eltFilter =            [];       // store the CSS filters
431            this.eltEffect =            [];       // store data about hover effects animations
432            this.authkey =              '';       // for Google Photos private (hidden) albums
433            this.paginationLastPage =   0;        // for albums
434            this.paginationLastWidth =  0;        // for albums
435            this.customData =           {};
436            this.selected =             false;
437            this.imageWidth =           0;        // image natural (real) width
438            this.imageHeight =          0;        // image natural (real) height
439            this.$elt =                 null;     // pointer to the corresponding DOM element
440            this.$Elts =                [];       // cached pointers to the thumbnail content -> to avoid jQuery().find()
441            this.tags =                 [];       // list of tags of the current item
442            this.albumTagList =         [];       // list of all the tags of the items contained in the current album
443            this.albumTagListSel =      [];       // list of currently selected tags (only for albums)
444            this.exif = { exposure: '', flash: '', focallength: '', fstop: '', iso: '', model: '', time: '', location: ''};
445            this.deleted =              false;    // item is deleted -> do not display anymore
446            this.rotationAngle =        0;        // image display rotation angle
447          }
448
449          // public static
450          
451          NGY2Item.Get = function( instance, ID ) {
452            var l = instance.I.length;
453            for( var i = 0; i < l; i++ ) {
454              if( instance.I[i].GetID() == ID ) {
455                return instance.I[i];
456              }
457            }
458            return null;
459          };
460            
461          NGY2Item.GetIdx = function( instance, ID ) {
462            var l = instance.I.length;
463            for( var i = 0; i < l; i++ ) {
464              if( instance.I[i].GetID() == ID ) {
465                return i;
466              }
467            }
468            return -1;
469          };
470          
471          // create new item (image, album or albumUp)
472          NGY2Item.New = function( instance, title, description, ID, albumID, kind, tags ) {
473            var album = NGY2Item.Get( instance, albumID );
474            
475            if( albumID != -1 && albumID != 0 && title !='image gallery by nanogallery2 [build]'  ) {
476              if( instance.O.thumbnailLevelUp && album.getContentLength(false) == 0 && instance.O.album == '' ) {
477                // add navigation thumbnail (album up)
478                var item = new NGY2Item('0');
479                instance.I.push( item );
480                album.contentLength += 1;
481                item.title = 'UP';
482                item.albumID = albumID;
483                item.kind = 'albumUp';
484                item.G = instance;
485
486                jQuery.extend( true, item.thumbs.width, instance.tn.defaultSize.width);
487                jQuery.extend( true, item.thumbs.height, instance.tn.defaultSize.height);
488              }
489            }
490            
491            var item = NGY2Item.Get(instance, ID);
492            if( item === null ){
493              // create a new item (otherwise, just update the existing one)
494              item = new NGY2Item(ID);
495              instance.I.push(item);
496              if( albumID != -1 && title !='image gallery by nanogallery2 [build]' ) {
497                album.contentLength+=1;
498              }
499            }
500            item.G = instance;
501
502            item.albumID = albumID;
503            item.kind = kind;
504            if( kind == 'image' ) {
505              album.imageCounter += 1;
506              item.mediaNumber = album.imageCounter;
507            }
508
509            // check keyword to find features images/albums
510            var kw = instance.O.thumbnailFeaturedKeyword;
511            if( kw != '' ) {
512              // check if item featured based on a keyword in the title or in the description
513              kw = kw.toUpperCase();
514              var p = title.toUpperCase().indexOf(kw);
515              if( p > -1) {
516                item.featured = true;
517                // remove keyword case unsensitive
518                title = title.substring(0, p) + title.substring(p+kw.length, title.length);
519              }
520              p = description.toUpperCase().indexOf(kw);
521              if( p > -1) {
522                item.featured=true;
523                // remove keyword case unsensitive
524                description=description.substring(0, p) + description.substring(p + kw.length, description.length);
525              }
526            }
527            
528            // TAGS 
529            // if( instance.galleryFilterTags.Get() != false ) {
530              // if( instance.galleryFilterTags.Get() == true ) {
531                // if( tags != '' && tags != undefined ) {
532                  // use set tags
533                  // item.setTags(tags.split(' '));
534                // }
535              // }
536              // else {
537                // extract tags starting with # (in title)
538              if( typeof instance.galleryFilterTags.Get() == 'string' ) {
539                switch( instance.galleryFilterTags.Get().toUpperCase() ) {
540                  case 'TITLE':
541                    var re = /(?:^|\W)#(\w+)(?!\w)/g, match, matches = [];
542                    var tags = "";
543                    while (match = re.exec(title)) {
544                      matches.push(match[1].replace(/^\s*|\s*$/, ''));   //trim trailing/leading whitespace
545                    }
546                    item.setTags(matches);  //tags;
547                    title = title.split('#').join('');   //replaceall
548                    break;
549                  case 'DESCRIPTION':
550                    var re = /(?:^|\W)#(\w+)(?!\w)/g, match, matches = [];
551                    var tags = "";
552                    while (match = re.exec(description)) {
553                      matches.push(match[1].replace(/^\s*|\s*$/, ''));   //trim trailing/leading whitespace
554                    }
555                    item.setTags(matches);  //tags;
556                    description = description.split('#').join('');   //replaceall
557                    break;
558                }
559              }
560                else {
561                  if( tags != '' && tags != undefined ) {
562                    // use set tags
563                    item.setTags(tags.split(' '));
564                  }
565                }
566              // }
567            // }
568            
569            // set (maybe modified) fields title and description
570            item.title = escapeHtml(instance, title);
571            item.description = escapeHtml(instance, description);
572            return item;
573          };
574          
575          
576          // removes logically current item
577          NGY2Item.prototype.delete = function( ) {
578            this.deleted = true;
579            
580            // update content length of parent album
581            this.G.I[NGY2Item.GetIdx(this.G, this.albumID)].contentLength--;
582            this.G.I[NGY2Item.GetIdx(this.G, this.albumID)].numberItems--;
583            
584            // check if in DOM and removes it
585            var nbTn = this.G.GOM.items.length;
586            var ID = this.GetID();
587            var foundIdx = -1;
588            var foundGOMidx = -1;
589            for( var i = 0; i < nbTn ; i++ ) {
590              var curTn = this.G.GOM.items[i];
591              var item=this.G.I[curTn.thumbnailIdx];
592              if( item.GetID() == ID ) {
593                // FOUND
594                if( !curTn.neverDisplayed ) {
595                  foundIdx= curTn.thumbnailIdx;
596                  foundGOMidx= i;
597                }
598              }
599              else {
600                if( foundIdx != -1 ) {
601                  if( !curTn.neverDisplayed ) {
602                    // update index value
603                    item.$getElt('.nGY2GThumbnail').data('index', i-1);
604                    item.$getElt('.nGY2GThumbnailImg').data('index', i-1);
605                  }
606                }
607              }
608            }
609            if( foundIdx != -1 ) {
610              // delete item in GOM and delete thumbnail
611              var G = this.G;
612              if( this.selected == true ) {
613                this.selected = false;
614                G.GOM.nbSelected--;    // update the global counter
615              }
616              if( G.I[foundIdx].$elt !== null ) {
617                G.I[foundIdx].$elt.remove();      // delete thumbnail DOM object
618              }
619              G.GOM.items.splice(foundGOMidx, 1);   // delete in GOM
620              if( G.GOM.lastDisplayedIdx != -1 ) {
621                G.GOM.lastDisplayedIdx-=1;
622              }
623            }
624          }
625
626          NGY2Item.prototype.addToGOM = function( ) {
627            // retrieve index
628            var ID = this.GetID();
629            var l = this.G.I.length;
630            for( var idx = 0; idx < l; idx++ ) {
631              var item = this.G.I[idx];
632              if( item.GetID() == ID ) {
633                var w = item.thumbImg().width;
634                var h = item.thumbImg().height;
635                // set default size if required
636                if( h == 0 ) {
637                  h = this.G.tn.defaultSize.getHeight();
638                }
639                if( w == 0 ) {
640                  w = this.G.tn.defaultSize.getWidth();
641                }
642                // add to GOM -> will be displayed on next refresh/resize
643                var tn = new this.G.GOM.GTn(idx, w, h);
644                this.G.GOM.items.push(tn);
645                break;
646              }
647            }
648            
649          }
650          
651          
652          // function to avoid XSS issue - Cross Site Scripting
653          // original: https://github.com/janl/mustache.js/blob/master/mustache.js#L55
654          var entityMap = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', '/': '&#x2F;', '`': '&#x60;', '=': '&#x3D;' };
655          function escapeHtml (instance, string) {
656            if( instance.O.allowHTMLinData == true ) {
657              return string;
658            }
659            else {
660              return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
661                return entityMap[s];
662              });
663            }
664          }
665          
666          
667          NGY2Item.get_nextId = function () {
668            return nextId;
669          };
670
671          //=== public (shared across instances)
672
673          //--- cached sub elements
674          NGY2Item.prototype.$getElt = function( elt, forceRefresh ) {
675            if( this.$elt == null ) { return null; }
676            if( this.$Elts[elt] !== undefined && !forceRefresh == true ) {
677              return this.$Elts[elt];
678            }
679            else {
680              if( elt == '.nGY2GThumbnail' ) {
681                this.$Elts[elt]=this.$elt;
682              }
683              else {
684                this.$Elts[elt]=this.$elt.find(elt);
685              }
686              return this.$Elts[elt];
687            }
688          };
689          
690          // remove one element (in DOM and in cache)
691          NGY2Item.prototype.removeElt = function( elt ) {
692            if( this.$elt == null ) { return; }
693            if( this.$Elts[elt] == undefined) { return; }
694            this.$Elts[elt].remove();
695            var index = this.$Elts.indexOf(elt);
696            this.$Elts.splice(index, 1);
697          };
698
699          //--- returns the album containing the item
700          NGY2Item.prototype.album = function() {
701            return this.G.I[NGY2Item.GetIdx(this.G, this.albumID)];
702          };
703          
704          //--- viewer - transition can be disabled per media kind - returns true if current media supports transition (swipe)
705          NGY2Item.prototype.mediaTransition = function( ) {
706            if( this.G.O.viewerTransitionMediaKind.indexOf( this.mediaKind ) > -1 ) {
707              return true;
708            }
709            return false;
710          };
711
712          //--- set one image (url and size)
713          NGY2Item.prototype.imageSet = function( src, w, h ) {
714            this.src = src;
715            this.width = w;
716            this.height = h;
717          };
718          
719          //--- set one thumbnail (url and size) - screenSize and level are optionnal
720          NGY2Item.prototype.thumbSet = function( src, w, h, screenSize, level ) {
721            var lst=['xs','sm','me','la','xl'];
722            if( typeof screenSize === 'undefined' || screenSize == '' || screenSize == null ) {
723              for( var i=0; i< lst.length; i++ ) {
724                if( typeof level === 'undefined' || level == '' ) {
725                  this.thumbs.url.l1[lst[i]]=src;
726                  this.thumbs.height.l1[lst[i]]=h;
727                  this.thumbs.width.l1[lst[i]]=w;
728                  this.thumbs.url.lN[lst[i]]=src;
729                  this.thumbs.height.lN[lst[i]]=h;
730                  this.thumbs.width.lN[lst[i]]=w;
731                }
732                else {
733                  this.thumbs.url[level][lst[i]]=src;
734                  this.thumbs.height[level][lst[i]]=h;
735                  this.thumbs.width[level][lst[i]]=w;
736                }
737              }
738            }
739            else {
740              if( typeof level === 'undefined' || level == '' || level == null ) {
741                this.thumbs.url.l1[screenSize]=src;
742                this.thumbs.height.l1[screenSize]=h;
743                this.thumbs.width.l1[screenSize]=w;
744                this.thumbs.url.lN[screenSize]=src;
745                this.thumbs.height.lN[screenSize]=h;
746                this.thumbs.width.lN[screenSize]=w;
747              }
748              else {
749                this.thumbs.url[level][screenSize]=src;
750                this.thumbs.height[level][screenSize]=h;
751                this.thumbs.width[level][screenSize]=w;
752              }
753            }
754          
755            var lst=['xs','sm','me','la','xl'];
756            for( var i=0; i< lst.length; i++ ) {
757              this.thumbs.height.l1[lst[i]]=h;
758            }
759            for( var i=0; i< lst.length; i++ ) {
760              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() ) {
761                this.thumbs.height.lN[lst[i]]=h;
762              }
763            }
764          };
765
766          //--- set thumbnail image real height for current level/resolution, and for all others level/resolutions having the same settings
767          NGY2Item.prototype.thumbSetImgHeight = function( h ) {              
768            var lst=['xs','sm','me','la','xl'];
769            for( var i=0; i< lst.length; i++ ) {
770              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() ) {
771                this.thumbs.height.l1[lst[i]]=h;
772              }
773            }
774            for( var i=0; i< lst.length; i++ ) {
775              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() ) {
776                this.thumbs.height.lN[lst[i]]=h;
777              }
778            }
779          };
780
781          //--- set thumbnail image real width for current level/resolution, and for all others level/resolutions having the same settings
782          NGY2Item.prototype.thumbSetImgWidth = function( w ) {              
783            var lst=['xs','sm','me','la','xl'];
784            for( var i=0; i< lst.length; i++ ) {
785              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() ) {
786                this.thumbs.width.l1[lst[i]]=w;
787              }
788            }
789            for( var i=0; i< lst.length; i++ ) {
790              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() ) {
791                this.thumbs.width.lN[lst[i]]=w;
792              }
793            }
794          };
795          
796          //--- Returns Thumbnail image (depending of the screen resolution)
797          NGY2Item.prototype.thumbImg = function () {   
798            var tnImg = { src: '', width: 0, height: 0 };
799
800            if( this.title == 'image gallery by nanogallery2 [build]' ) {
801              tnImg.src = this.G.emptyGif;
802              tnImg.url = this.G.emptyGif;
803              return tnImg;
804            }
805            tnImg.src = this.thumbs.url[this.G.GOM.curNavLevel][this.G.GOM.curWidth];
806            tnImg.width = this.thumbs.width[this.G.GOM.curNavLevel][this.G.GOM.curWidth];
807            tnImg.height = this.thumbs.height[this.G.GOM.curNavLevel][this.G.GOM.curWidth];
808            return tnImg;
809          };
810          
811          //--- Set tags to items and add these tags to the album
812          NGY2Item.prototype.setTags = function( tags ) {              
813          if( tags.length > 0 ) {
814              this.tags = tags;
815              var lstTags = this.album().albumTagList;
816              for( var i = 0; i < tags.length; i++ ) {
817                var tfound = false;
818                for( var j = 0; j < lstTags.length; j++ ) {
819                  if( tags[i].toUpperCase() == lstTags[j].toUpperCase() ) {
820                    tfound = true;
821                  }
822                }
823                if( tfound == false) {
824                  this.album().albumTagList.push(tags[i])
825                  this.album().albumTagListSel.push(tags[i])
826                }
827              }
828            }
829          };
830          
831          //--- check if 1 of current item's tags is selected (tag filter)
832          NGY2Item.prototype.checkTagFilter = function() {
833            if( this.G.galleryFilterTags.Get() != false && this.album().albumTagList.length > 0 ) {
834              if( this.G.O.thumbnailLevelUp && this.kind=='albumUp' ) {
835                return true;
836              }
837              var found = false;
838              var lstTags = this.album().albumTagListSel;
839              for( var i = 0; i < this.tags.length; i++ ) {
840                for( var j = 0; j < lstTags.length; j++ ) {
841                  if( this.tags[i].toUpperCase() == lstTags[j].toUpperCase() ) {
842                    found = true;
843                    break;
844                  }
845                }
846              }
847              return found;
848            }
849            else
850              return true;
851          };
852          
853          //--- check if 1 of current item's tags is found using API search
854          NGY2Item.prototype.isSearchTagFound = function() {
855            if( this.G.GOM.albumSearchTags == '' ) { return true; }
856            if( this.G.O.thumbnailLevelUp && this.kind == 'albumUp' ) { return true; }
857
858            //var lstTags=this.album().albumTagListSel;
859            for( var i = 0; i < this.tags.length; i++ ) {
860              if( this.tags[i].toUpperCase().indexOf( this.G.GOM.albumSearchTags ) >= 0 ) {
861                return true;
862              }
863            }
864            return false;
865          };
866          
867          //--- set the URL of the media to display in the viewer
868          //--- markup is defined for images
869          NGY2Item.prototype.setMediaURL = function( url, mediaKind, description ) {
870            this.src = url;
871            this.mediaKind = mediaKind;
872            this.description = description;
873            if( mediaKind == 'img' ) {
874              this.mediaMarkup = '<img class="nGY2ViewerMedia" src="' + url + '" alt="' + description + '
vendor: 10,429 bytes, lines 874-1108
874" itemprop="contentURL" draggable="false">';
875            }
876          };
877          
878          
879          //--- check if current item should be displayed
880          NGY2Item.prototype.isToDisplay = function( albumID ) {
881            return this.albumID == albumID && this.checkTagFilter() && this.isSearchFound() && this.isSearchTagFound() && this.deleted == false;
882          };
883          
884          
885          
886          //--- returns the number of items of the current album
887          //--- count using tags filter
888          NGY2Item.prototype.getContentLength = function( filterTags ) {
889            if( filterTags == false || this.albumTagList.length == 0 || this.G.galleryFilterTags.Get() == false ) {
890              return this.contentLength;
891            }
892            else {
893              var l = this.G.I.length;
894              var cnt = 0;
895              var albumID = this.GetID();
896              for( var idx = 0; idx < l; idx++ ) {
897                var item = this.G.I[idx];
898                if( item.isToDisplay(albumID) ) {
899                  cnt++;
900                }
901              }
902              return cnt;
903            }
904          };
905          
906          NGY2Item.prototype.isSearchFound = function() {
907            if( this.G.GOM.albumSearch != '' ) {
908              if( this.title.toUpperCase().indexOf( this.G.GOM.albumSearch ) == -1 ) {
909                return false;
910              }
911            }
912            return true;
913          }
914          
915          
916          //--- for future use...
917          NGY2Item.prototype.responsiveURL = function () {
918            var url = '';
919            switch(this.G.O.kind) {
920              case '':
921                url = this.src;
922                break;
923              case 'flickr':
924                url = this.src;
925                break;
926              case 'picasa':
927              case 'google':
928              case 'google2':
929              default:
930                url = this.src;
931                break;
932            }
933            return url;
934          };
935          
936          
937          //--- Reveal the thumbnail image with animation on opacity
938          NGY2Item.prototype.ThumbnailImageReveal = function () {
939
940            if( this.thumbnailImgRevealed == false ) {
941              this.thumbnailImgRevealed = true;
942              new NGTweenable().tween({
943                from:         { opacity: 0 },
944                to:           { opacity: 1 },
945                attachment:   { item: this },
946                delay:        30,
947                duration:     400,
948                easing:       'easeOutQuart',
949                step:         function (state, att) {
950                  var $e=att.item.$getElt('.nGY2TnImg');
951                  if( $e != null ) {
952                    $e.css( state );
953                  }
954                }
955              });
956            }
957          };
958          
959          
960          // In case of thumbnails with stacks - apply a percent to a value which include a unit
961          function ValueApplyPercent( str, percent ) {
962            str=String(str);
963            if( str === '0' || percent == 1 ) { return str; }
964            var n = Number(str.replace(/[a-zA-Z]/g, ''));
965            var ar = str.match(/([^\-0-9\.]+)/g);
966            var a = '';
967            if( ar != null && ar.length > 0 ) {
968              a = ar.join();
969            }
970             
971            if( isNaN(n) || n == 0 ) {
972              return str;
973            }
974
975            n = n * percent;
976            return n + a;
977          } 
978          
979          //--- 2D/3D CSS transform - apply the cached value to element
980          NGY2Item.prototype.CSSTransformApply = function ( eltClass ) {
981            var obj = this.eltTransform[eltClass];
982
983            if( eltClass == '.nGY2GThumbnail' ) {
984              // thumbnail
985              var nbStacks = obj.$elt.length-1;
986              var pTranslateX = 1;
987              var pTranslateY = 1;
988              var pTranslateZ = 1;
989              var pTranslate = 1;
990              var pRotateX = 1;
991              var pRotateY = 1;
992              var pRotateZ = 1;
993              var pRotate = 1;
994              var pScale = 1;
995              for( var n = nbStacks; n >= 0; n-- ) {
996                // units must be given with
997                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) + ')';
998                if( !(this.G.IE <= 9) && !this.G.isGingerbread ) {
999                  v += ' rotateX(' + ValueApplyPercent(obj.rotateX,pRotateX) + ') rotateY(' + ValueApplyPercent(obj.rotateY,pRotateY) + ') rotateZ(' + ValueApplyPercent(obj.rotateZ,pRotateZ) + ') rotate(' + ValueApplyPercent(obj.rotate,pRotate) + ')';
1000                }
1001                else {
1002                  v += ' rotate(' + ValueApplyPercent(obj.rotateZ,pRotateZ) + ')';
1003                }
1004                obj.$elt[n].style[this.G.CSStransformName] = v;
1005                
1006                if( nbStacks > 0 ) {
1007                  // apply a percent to the stack elements
1008                  pTranslateX -= this.G.tn.opt.Get('stacksTranslateX');
1009                  pTranslateY -= this.G.tn.opt.Get('stacksTranslateY');
1010                  pTranslateZ -= this.G.tn.opt.Get('stacksTranslateZ');
1011                  pRotateX    -= this.G.tn.opt.Get('stacksRotateX');
1012                  pRotateY    -= this.G.tn.opt.Get('stacksRotateY');
1013                  pRotateZ    -= this.G.tn.opt.Get('stacksRotateZ');
1014                  pScale      -= this.G.tn.opt.Get('stacksScale');
1015                }
1016              }
1017            }
1018            else {
1019              // thumbnail sub element
1020              if( obj.$elt != null ) {
1021                for( var n = 0; n < obj.$elt.length; n++ ) {
1022                  if( obj.$elt[n] != undefined ) {
1023                    // units must be given with
1024                    var v = 'translateX(' + obj.translateX + ') translateY(' + obj.translateY + ') translateZ(' + obj.translateZ + ') scale(' + obj.scale + ') translate(' + obj.translate + ')';
1025                    if( !(this.G.IE <= 9) && !this.G.isGingerbread ) {
1026                      v += ' rotateX(' + obj.rotateX + ') rotateY(' + obj.rotateY + ') rotateZ(' + obj.rotateZ + ') rotate(' + obj.rotate + ')';
1027                    }
1028                    else {
1029                      v += ' rotate(' + obj.rotateZ + ')';
1030                    }
1031                    obj.$elt[n].style[this.G.CSStransformName] = v;
1032                    }
1033                }
1034              }
1035            }
1036          };
1037
1038          //--- 2D/3D CSS transform - set a value in cache
1039          NGY2Item.prototype.CSSTransformSet = function ( eltClass, transform, value, forceRefresh ) {
1040            if( this.eltTransform[eltClass] == undefined ) {
1041              this.eltTransform[eltClass] = { translateX: 0, translateY: 0, translateZ: 0, rotateX: 0, rotateY: 0, rotateZ: 0, scale: 1, translate: '0px,0px', rotate: 0 };
1042              this.eltTransform[eltClass].$elt = this.$getElt(eltClass);
1043            }
1044            this.eltTransform[eltClass][transform] = value;
1045            if( forceRefresh === true ) {
1046              this.eltTransform[eltClass].$elt = this.$getElt(eltClass, true);
1047            }
1048          };
1049
1050          //--- CSS Filters - apply the cached value to element
1051          NGY2Item.prototype.CSSFilterApply = function ( eltClass ) {
1052            var obj = this.eltFilter[eltClass];
1053            var v = 'blur(' + obj.blur + ') brightness(' + obj.brightness + ') grayscale(' + obj.grayscale + ') sepia(' + obj.sepia + ') contrast(' + obj.contrast + ') opacity(' + obj.opacity + ') saturate(' + obj.saturate + ')';
1054            if( obj.$elt != null ) {
1055              for( var n = 0; n < obj.$elt.length; n++ ) {
1056                if( obj.$elt[n] != undefined ) {
1057                  obj.$elt[n].style.WebkitFilter = v;
1058                  obj.$elt[n].style.filter = v;
1059                }
1060              }
1061            }
1062          };
1063
1064          //--- CSS Filters - set a value in cache
1065          NGY2Item.prototype.CSSFilterSet = function ( eltClass, filter, value, forceRefresh ) {
1066            if( this.eltFilter[eltClass] == undefined ) {
1067              this.eltFilter[eltClass] = { blur: 0, brightness: '100%', grayscale: '0%', sepia: '0%', contrast: '100%', opacity: '100%', saturate: '100%' };
1068              this.eltFilter[eltClass].$elt = this.$getElt(eltClass);
1069            }
1070            this.eltFilter[eltClass][filter] = value;
1071            if( forceRefresh === true ) {
1072              this.eltTransform[eltClass].$elt = this.$getElt(eltClass, true);
1073            }
1074          };
1075
1076          //--- thumbnail hover animation
1077          NGY2Item.prototype.animate = function ( effect, delay, hoverIn ) {
1078            if( this.$getElt() == null  ) { return; }
1079
1080            var context = {};
1081            context.G = this.G;
1082            context.item = this;
1083            context.effect = effect;
1084            context.hoverIn = hoverIn;
1085            context.cssKind = '';
1086            if( hoverIn ) {
1087              // HOVER IN
1088              
1089              if( this.eltEffect[effect.element] == undefined ) {
1090                this.eltEffect[effect.element] = [];
1091              }
1092              if( this.eltEffect[effect.element][effect.type] == undefined ) {
1093                this.eltEffect[effect.element][effect.type] = { initialValue: 0, lastValue: 0 };
1094              }
1095              if( effect.firstKeyframe ) {
1096                // store initial and current value -> for use in the back animation
1097                this.eltEffect[effect.element][effect.type] = { initialValue: effect.from, lastValue: effect.from};
1098              }
1099              
1100              context.animeFrom = effect.from;
1101              context.animeTo = effect.to;
1102              context.animeDuration = parseInt(effect.duration);
1103              context.animeDelay = 30 + parseInt(effect.delay + delay);  // 30ms is a default delay to avoid conflict with other initializations
1104              context.animeEasing = effect.easing;
1105            }
1106            else {
1107              // HOVER OUT
1108              
vendor: 8,341 bytes, lines 1108-1310
1108if( effect.firstKeyframe ) {
1109                context.animeFrom = this.eltEffect[effect.element][effect.type].lastValue;
1110                context.animeTo = this.eltEffect[effect.element][effect.type].initialValue;
1111                // context.animeTo=effect.from;
1112              }
1113              else {
1114                // context.animeFrom=effect.from;
1115                context.animeFrom = this.eltEffect[effect.element][effect.type].lastValue;
1116                context.animeTo = this.eltEffect[effect.element][effect.type].initialValue;
1117                // context.animeTo=effect.to;
1118                
1119              }
1120              
1121              context.animeDuration = parseInt(effect.durationBack);
1122              context.animeDelay = 30 + parseInt(effect.delayBack + delay);   // 30ms is a default delay to avoid conflict with other initializations
1123              context.animeEasing = effect.easingBack;
1124            }
1125            
1126
1127            // detect if animation on CSS transform
1128            var transform=['translateX', 'translateY', 'translateZ', 'scale', 'rotateX', 'rotateY', 'rotateZ'];
1129            for( var i = 0; i < transform.length; i++ ) {
1130              if( effect.type == transform[i] ) {
1131                context.cssKind = 'transform';
1132                break;
1133              }
1134            }
1135
1136            // detect if animation on CSS filter
1137            var filter=['blur', 'brightness', 'grayscale', 'sepia', 'contrast', 'opacity', 'saturate'];
1138            for( var i = 0; i < filter.length; i++ ) {
1139              if( effect.type == filter[i] ) {
1140                context.cssKind = 'filter';
1141                break;
1142              }
1143            }
1144            // handle some special cases
1145            if( hoverIn && effect.element == '.nGY2GThumbnail' && ( effect.type == 'scale' || effect.type == 'rotateX') ) {
1146              this.G.GOM.lastZIndex++;
1147              this.$getElt(effect.element).css('z-index', this.G.GOM.lastZIndex);
1148              // setElementOnTop(this.G.$E.base, this.$getElt(effect.element) );
1149            }
1150            
1151            // animation
1152            var tweenable = new NGTweenable();
1153            context.tweenable=tweenable;
1154            tweenable.tween({
1155              attachment:   context,
1156              from:         { 'v': context.animeFrom },
1157              to:           { 'v': context.animeTo },
1158              duration:     context.animeDuration,    //parseInt(effect.duration),
1159              delay:        context.animeDelay,       //parseInt(effect.delay),
1160              easing:       context.animeEasing,      //'easeOutQuart',
1161              
1162              step: function (state, att) {
1163                if( att.item.$getElt() == null ) {
1164                  // the thumbnail may be destroyed since the start of the animation
1165                  att.tweenable.stop(false);
1166                  // att.tweenable.dispose();
1167                  return;
1168                }
1169                if( att.hoverIn && !att.item.hovered ) {
1170                  // thumbnail no more hovered
1171                  att.tweenable.stop(false);
1172                  // att.tweenable.dispose();
1173                  return;
1174                }
1175
1176                if( att.G.VOM.viewerDisplayed ) {
1177                  att.tweenable.stop(false);
1178                  // att.tweenable.dispose();
1179                  return;
1180                }
1181                
1182                // test if in delay phase
1183                if( state.v == att.animeFrom ) { return; }
1184                
1185                switch( att.cssKind ) {
1186                  case 'transform':
1187                    // window.ng_draf( function() {
1188                      att.item.CSSTransformSet(att.effect.element, att.effect.type, state.v);
1189                      att.item.CSSTransformApply( att.effect.element );
1190                    // });
1191                    break;
1192                  case 'filter':
1193                    // window.ng_draf( function() {
1194                      att.item.CSSFilterSet(att.effect.element, att.effect.type, state.v);
1195                      att.item.CSSFilterApply( att.effect.element );
1196                    // });
1197                    break;
1198                  default:
1199                    var v=state.v;
1200                    if( state.v.substring(0,4) == 'rgb(' || state.v.substring(0,5) == 'rgba(' ) {
1201                      // to remove values after the dot (not supported by RGB/RGBA)
1202                      // v=ngtinycolor(state.v).toRgbString();
1203                      v = ShadeBlendConvert(0, v);
1204                    }
1205                    // window.ng_draf( function() {
1206                      att.item.$getElt( att.effect.element ).css( att.effect.type, v );
1207                    // });
1208                    break;
1209                }
1210                if( hoverIn ) {
1211                  // store value for back animation
1212                  att.item.eltEffect[att.effect.element][att.effect.type].lastValue = state.v;
1213                }
1214              },
1215              
1216              finish: function (state, att) {
1217                if( hoverIn ) {
1218                  // store value for back animation
1219                  att.item.eltEffect[att.effect.element][att.effect.type].lastValue = state.v;
1220                }
1221
1222                if( att.item.$getElt() == null ) {
1223                  // the thumbnail may be destroyed since the start of the animation
1224                  return;
1225                }
1226                if( att.hoverIn && !att.item.hovered ) {
1227                  // thumbnail no more hovered
1228                  return;
1229                }
1230
1231                if( att.G.VOM.viewerDisplayed ) {
1232                  return;
1233                }
1234
1235                // window.ng_draf( function() {
1236                  switch( att.cssKind ) {
1237                    case 'transform':
1238                      att.item.CSSTransformSet(att.effect.element, att.effect.type, att.animeTo);
1239                      att.item.CSSTransformApply(att.effect.element);
1240                      break;
1241                    case 'filter':
1242                      att.item.CSSFilterSet(att.effect.element, att.effect.type, att.animeTo);
1243                      att.item.CSSFilterApply(att.effect.element);
1244                      break;
1245                    default:
1246                      att.item.$getElt(att.effect.element).css(att.effect.type, att.animeTo);
1247                      break;
1248                  }
1249                // });
1250              }
1251            });
1252          };
1253
1254          return NGY2Item;
1255        })();    
1256          
1257      }
1258
1259      _this.options = jQuery.extend(true, {}, jQuery.nanogallery2.defaultOptions, options);
1260      // Initialization code
1261      _this.nG2 = null;
1262      _this.nG2 = new nanoGALLERY2();
1263      _this.nG2.initiateGallery2(_this.e, _this.options );
1264
1265    };
1266      
1267    // PUBLIC EXPOSED METHODS
1268    _this.test = function() {
1269      //alert('test');
1270      // console.dir(_this.nG.G.I.length);
1271      // console.dir(_this.nG);
1272      //privateTest();
1273    }
1274
1275    
1276    // Run initializer
1277    _this.init();
1278  };
1279 
1280  jQuery.nanogallery2.defaultOptions = {
1281    kind :                        '',
1282    userID :                      '',
1283    photoset :                    '',
1284    album:                        '',
1285    blackList :                   'scrapbook|profil|auto backup',
1286    whiteList :                   '',
1287    albumList :                   '',
1288    albumList2 :                  null,
1289    RTL :                         false,
1290    poogleplusUseUrlCrossDomain : true,
1291    flickrSkipOriginal :          true,
1292    breadcrumbAutoHideTopLevel :  true,
1293    displayBreadcrumb :           true,
1294    breadcrumbOnlyCurrentLevel :  true,
1295    breadcrumbHideIcons :         true,
1296    theme :                       'nGY2',
1297    galleryTheme :                'dark',
1298    viewerTheme :                 'dark',
1299    items :                       null,
1300    itemsBaseURL :                '',
1301    thumbnailSelectable :         false,
1302    dataProvider:                 '',
1303    dataCharset:                  'Latin',
1304    allowHTMLinData:              false,
1305    locationHash :                true,
1306    slideshowDelay :              3000,
1307    slideshowAutoStart :          false,
1308
1309    debugMode:                    false,
1310
1311    galleryDisplayMoreStep :      2,
1312    galleryDisplayMode :          'fullContent',
1313    galleryL1DisplayMode :        null,
1314    galleryPaginationMode :       'rectangles',   // 'dots', 'rectangles', 'numbers'
1315    // galleryThumbnailsDisplayDelay : 2000,
1316    galleryMaxRows :              2,
1317    galleryL1MaxRows :            null,
1318    galleryLastRowFull:           false,
1319    galleryLayoutEngine :         'default',
1320    paginationSwipe:              true,
1321    paginationVisiblePages :      10,
1322    // paginationSwipeSensibilityVert : 10,
1323    galleryFilterTags :           false,    // possible values: false, true, 'title', 'description'
1324    galleryL1FilterTags :         null,     // possible values: false, true, 'title', 'description'
1325    galleryMaxItems :             0,        // maximum number of items per album  --> only flickr, google2, nano_photos_provider2
1326    galleryL1MaxItems :           null,     // maximum number of items per gallery page --> only flickr, google2, nano_photos_provider2
1327    gallerySorting :              '',
1328    galleryL1Sorting :            null,
1329    galleryDisplayTransition :    'none',
1330    galleryL1DisplayTransition :  null,
1331    galleryDisplayTransitionDuration :    1000,
1332    galleryL1DisplayTransitionDuration :  null,
1333    galleryResizeAnimation :      true,
1334    galleryRenderDelay :          60,
1335
1336    thumbnailCrop :               true,
1337    thumbnailL1Crop :             null,
1338    thumbnailCropScaleFactor :    1.5,
1339    thumbnailLevelUp :            false,
1340    thumbnailAlignment :          'fillWidth',
1341    thumbnailWidth :              300,
1342    thumbnailL1Width :            null,
1343    thumbnailHeight :             200,
1344    thumbnailL1Height :           null,
1345    thumbnailBaseGridHeight :     0,
1346    thumbnailL1BaseGridHeight :   null,
1347    thumbnailGutterWidth :        2,
1348    thumbnailL1GutterWidth :      null,
1349    thumbnailGutterHeight :       2,
1350    thumbnailL1GutterHeight :     null,
1351    thumbnailBorderVertical :     2,
1352    thumbnailBorderHorizontal :   2,
1353    thumbnailFeaturedKeyword :    '*featured',
1354    thumbnailAlbumDisplayImage :  false,
1355    thumbnailHoverEffect2 :       'toolsAppear',
1356    thumbnailBuildInit2 :         '',
1357    thumbnailStacks :             0,
1358    thumbnailL1Stacks :           null,
1359    thumbnailStacksTranslateX :   0,
1360    thumbnailL1StacksTranslateX : null,
1361    thumbnailStacksTranslateY :   0,
1362    thumbnailL1StacksTranslateY : null,
1363    thumbnailStacksTranslateZ :   0,
1364    thumbnailL1StacksTranslateZ : null,
1365    thumbnailStacksRotateX :      0,
1366    thumbnailL1StacksRotateX :    null,
1367    thumbnailStacksRotateY :      0,
1368    thumbnailL1StacksRotateY :    null,
1369    thumbnailStacksRotateZ :      0,
1370    thumbnailL1StacksRotateZ :    null,
1371    thumbnailStacksScale :        0,
1372    thumbnailL1StacksScale :      null,
1373    thumbnailDisplayOutsideScreen: true,
1374    thumbnailWaitImageLoaded:     true,
1375    thumbnailSliderDelay:         2000,
1376    galleryBuildInit2 :           '',
1377    portable :                    false,
1378    eventsDebounceDelay:          30,
1379    
1380    touchAnimation :              true,
1381    touchAnimationL1 :            undefined,
1382    touchAutoOpenDelay :          0,
1383
1384    thumbnailLabel : {
1385      position :                  'overImageOnBottom',
1386      align:                      'center',
1387      display :                   true,
1388      displayDescription :        false,
1389      titleMaxLength :            0,
1390      titleMultiLine :            false,
1391      descriptionMaxLength :      0,
1392      descriptionMultiLine :      false,
1393      hideIcons :                 true,
1394      title :                     ''
1395    },
1396
1397    thumbnailToolbarImage :       { topLeft: 'select', topRight : 'featured' },
1398    thumbnailToolbarAlbum :       { topLeft: 'select', topRight : 'counter'  },
1399    thumbnailDisplayInterval :    15,
1400    thumbnailL1DisplayInterval :  null,
1401    thumbnailDisplayTransition :  'fadeIn',
1402    thumbnailL1DisplayTransition : null,
1403    thumbnailDisplayTransitionDuration:   240,
1404    thumbnailL1DisplayTransitionDuration: null,
1405    thumbnailOpenImage :          true,
1406    thumbnailOpenOriginal :       false,
1407    thumbnailGlobalImageTitle :   '',
1408    thumbnailGlobalAlbumTitle :   '',
1409    
1410    viewer :                      'internal',
1411    viewerFullscreen:             false,
1412    viewerDisplayLogo :           false,
1413    imageTransition :             'swipe2',
1414    viewerTransitionMediaKind :   'img',
1415    viewerZoom :                  true,
1416    viewerImageDisplay :          '',
1417    openOnStart :                 '',
1418    viewerHideToolsDelay :        3000,
1419    viewerToolbar : {
1420      display :                   true,
1421      position :                  'bottomOverImage',
1422      fullWidth :                 true,
1423      align :                     'center',
1424      autoMinimize :              0,
1425      standard :                  'minimizeButton,label',
1426      minimized :                 'minimizeButton,label,infoButton,shareButton,downloadButton,linkOriginalButton,fullscreenButton'
1427    },
1428    viewerTools : {
1429      topLeft :                   'pageCounter,playPauseButton',
1430      topRight :                  'rotateLeft,rotateRight,fullscreenButton,closeButton' 
1431    },
1432    
1433    breakpointSizeSM :            480,
1434    breakpointSizeME :            992,
1435    breakpointSizeLA :            1200,
1436    breakpointSizeXL :            1800,
1437    
1438    fnThumbnailInit :             null,
1439    fnThumbnailHoverInit :        null,
1440    fnThumbnailHover :            null,
1441    fnThumbnailHoverOut :         null,
1442    fnThumbnailDisplayEffect :    null,
1443    fnViewerInfo :                null,
1444    fnImgToolbarCustInit :        null,
1445    fnImgToolbarCustDisplay :     null,
1446    fnImgToolbarCustClick :       null,
1447    fnProcessData :               null,
1448    fnThumbnailSelection :        null,
1449    fnGalleryRenderStart :        null,
1450    fnGalleryRenderEnd :          null,
1451    fnGalleryObjectModelBuilt :   null,
1452    fnGalleryLayoutApplied :      null,
1453    fnThumbnailClicked :          null,
1454    fnShoppingCartUpdated :       null,
1455    fnThumbnailToolCustAction :   null,
1456    fnThumbnailOpen :             null,
1457    fnImgDisplayed :              null,
1458
1459    i18n : {
1460      'breadcrumbHome' : 'Galleries', 'breadcrumbHome_FR' : 'Galeries',
1461      'thumbnailImageTitle' : '', 'thumbnailAlbumTitle' : '',
1462      'thumbnailImageDescription' : '', 'thumbnailAlbumDescription' : '',
1463      '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'
1464    },
1465    icons : {
1466      // sample for font awesome: <i style="color:#eee;" class="fa fa-search-plus"></i>
1467      thumbnailAlbum:               '<i class="nGY2Icon-folder-empty"></i>',
1468      thumbnailImage:               '<i class="nGY2Icon-picture"></i>',
1469      breadcrumbAlbum:              '<i class="nGY2Icon-folder-empty"></i>',
1470      breadcrumbHome:               '<i class="nGY2Icon-home"></i>',
1471      breadcrumbSeparator:          '<i class="nGY2Icon-left-open"></i>',
1472      breadcrumbSeparatorRtl:       '<i class="nGY2Icon-right-open"></i>',
1473      navigationFilterSelected:     '<i style="color:#fff;" class="nGY2Icon-toggle-on"></i>',
1474      navigationFilterUnselected:   '<i style="color:#ddd;" class="nGY2Icon-toggle-off"></i>',
1475      navigationFilterSelectedAll:  '<i class="nGY2Icon-toggle-on"></i><i class="nGY2Icon-ok"></i>',
1476      thumbnailSelected:            '<i style="color:#bff;" class="nGY2Icon-ok-circled"></i>',
1477      thumbnailUnselected:          '<i style="color:#bff;" class="nGY2Icon-circle-empty"></i>',
1478      thumbnailFeatured:            '<i style="color:#dd5;" class="nGY2Icon-star"></i>',
1479      thumbnailCounter:             '<i class="nGY2Icon-picture"></i>',
1480      thumbnailShare:               '<i class="nGY2Icon-ngy2_share2"></i>',
1481      thumbnailDownload:            '<i class="nGY2Icon-ngy2_download2"></i>',
1482      thumbnailInfo:                '<i class="nGY2Icon-ngy2_info2"></i>',
1483      thumbnailCart:                '<i class="nGY2Icon-basket"></i>',
1484      thumbnailDisplay:             '<i class="nGY2Icon-ngy2_zoom_in2"></i>',
1485      thumbnailCustomTool1:         'T1',
1486      thumbnailCustomTool2:         'T2',
1487      thumbnailCustomTool3:         'T3',
1488      thumbnailCustomTool4:         'T4',
1489      thumbnailCustomTool5:         'T5',
1490      thumbnailCustomTool6:         'T6',
1491      thumbnailCustomTool7:         'T7',
1492      thumbnailCustomTool8:         'T8',
1493      thumbnailCustomTool9:         'T9',
1494      thumbnailCustomTool10:        'T10',
1495      thumbnailAlbumUp:             '<i style="font-size: 3em;" class="nGY2Icon-ngy2_chevron_up2"></i>',
1496      paginationNext:               '<i class="nGY2Icon-right-open"></i>',
1497      paginationPrevious:           '<i class="nGY2Icon-left-open"></i>',
1498      galleryMoreButton:            '<i class="nGY2Icon-picture"></i> &nbsp; <i class="nGY2Icon-right-open"></i>',
1499      buttonClose:                  '<i class="nGY2Icon-ngy2_close2"></i>',
1500      viewerPrevious:               '<i class="nGY2Icon-ngy2_chevron-left"></i>',
1501      viewerNext:                   '<i class="nGY2Icon-ngy2_chevron-right"></i>',
1502      viewerImgPrevious:            '<i class="nGY2Icon-ngy2_chevron_left3"></i>',
1503      viewerImgNext:                '<i class="nGY2Icon-ngy2_chevron_right3"></i>',
1504      viewerDownload:               '<i class="nGY2Icon-ngy2_download2"></i>',
1505      viewerToolbarMin:             '<i class="nGY2Icon-ellipsis-vert"></i>',
1506      viewerToolbarStd:             '<i class="nGY2Icon-menu"></i>',
1507      viewerPlay:                   '<i class="nGY2Icon-play"></i>',
1508      viewerPause:                  '<i class="nGY2Icon-pause"></i>',
1509      viewerFullscreenOn:           '<i class="nGY2Icon-resize-full"></i>',
1510      viewerFullscreenOff:          '<i class="nGY2Icon-resize-small"></i>',
1511      viewerZoomIn:                 '<i class="nGY2Icon-ngy2_zoom_in2"></i>',
1512      viewerZoomOut:                '<i class="nGY2Icon-ngy2_zoom_out2"></i>',
1513      viewerLinkOriginal:           '<i class="nGY2Icon-ngy2_external2"></i>',
1514      viewerInfo:                   '<i class="nGY2Icon-ngy2_info2"></i>',
vendor: 43,049 bytes, lines 1515-2619
1515      viewerShare:                  '<i class="nGY2Icon-ngy2_share2"></i>',
1516      viewerRotateLeft:             '<i class="nGY2Icon-ccw"></i>',
1517      viewerRotateRight:            '<i class="nGY2Icon-cw"></i>',
1518      user:                         '<i class="nGY2Icon-user"></i>',
1519      location:                     '<i class="nGY2Icon-location"></i>',
1520      config:                       '<i class="nGY2Icon-wrench"></i>',
1521      shareFacebook:                '<i style="color:#3b5998;" class="nGY2Icon-facebook-squared"></i>',
1522      shareTwitter:                 '<i style="color:#00aced;" class="nGY2Icon-twitter-squared"></i>',
1523      // shareGooglePlus:              '<i style="color:#dd4b39;" class="nGY2Icon-gplus-squared"></i>',
1524      shareTumblr:                  '<i style="color:#32506d;" class="nGY2Icon-tumblr-squared"></i>',
1525      sharePinterest:               '<i style="color:#cb2027;" class="nGY2Icon-pinterest-squared"></i>',
1526      shareVK:                      '<i style="color:#3b5998;" class="nGY2Icon-vkontakte"></i>',
1527      shareMail:                    '<i style="color:#555;" class="nGY2Icon-mail-alt"></i>',
1528      viewerCustomTool1:            'T1',
1529      viewerCustomTool2:            'T2',
1530      viewerCustomTool3:            'T3',
1531      viewerCustomTool4:            'T4',
1532      viewerCustomTool5:            'T5',
1533      viewerCustomTool6:            'T6',
1534      viewerCustomTool7:            'T7',
1535      viewerCustomTool8:            'T8',
1536      viewerCustomTool9:            'T9',
1537      viewerCustomTool10:           'T10'
1538    }
1539  };
1540
1541  jQuery.fn.nanogallery2 = function (args, option, value) {
1542    
1543    if( typeof jQuery(this).data('nanogallery2data') === 'undefined'){
1544      if( args == 'destroy' ) {
1545        // command to destroy but no instance yet --> exit
1546        return;
1547      }
1548      
1549      return this.each( function(){
1550        (new jQuery.nanogallery2(this, args));
1551      });
1552    }
1553    else {
1554      // no options -->
1555      // This function breaks the chain, but provides some API methods
1556
1557      var nG2=$(this).data('nanogallery2data').nG2;
1558      switch(args){
1559        case 'displayItem':
1560          nG2.DisplayItem(option);
1561          break;
1562          
1563        case 'search':
1564          return( nG2.Search(option));
1565          break;
1566          
1567        case 'search2':
1568          return nG2.Search2(option, value);
1569          break;
1570          
1571        case 'search2Execute':
1572          return nG2.Search2Execute();
1573          break;
1574          
1575        case 'refresh':
1576          nG2.Refresh();
1577          break;
1578
1579        case 'resize':
1580          nG2.Resize();
1581          break;
1582          
1583        case 'instance':
1584          return nG2;
1585          break;
1586          
1587        case 'data':
1588          nG2.data= {
1589            items: nG2.I,
1590            gallery: nG2.GOM,
1591            lightbox: nG2.VOM
1592          };
1593          return nG2.data;
1594          break;
1595          
1596        case 'reload':
1597          nG2.ReloadAlbum();
1598          return $(this);
1599          break;
1600          
1601        case 'itemsSelectedGet':
1602          return nG2.ItemsSelectedGet();
1603          break;
1604          
1605        case 'itemsSetSelectedValue':
1606          nG2.ItemsSetSelectedValue(option, value);
1607          break;
1608          
1609        case 'option':
1610          if(typeof value === 'undefined'){
1611            return nG2.Get(option);
1612          }else{
1613            nG2.Set(option,value);
1614            if( option == 'demoViewportWidth' ) {
1615              // force resize event -> for demo purposes
1616              $(window).trigger('resize');
1617            }
1618          }
1619          break;
1620          
1621        case 'destroy':
1622          nG2.Destroy();
1623          $(this).removeData('nanogallery2data');
1624          break;
1625          
1626        case 'shoppingCartGet':
1627          return nG2.shoppingCart;
1628          break;
1629          
1630        case 'shoppingCartUpdate':
1631          if( typeof value === 'undefined' || typeof option === 'undefined' ){
1632            return false;
1633          }
1634          var ID = option;
1635          var cnt = value;
1636          for( var i = 0; i < nG2.shoppingCart.length; i++) {
1637            if( nG2.shoppingCart[i].ID = ID ) {
1638              nG2.shoppingCart[i].cnt = cnt;
1639            }
1640          }
1641          var fu = G.O.fnShoppingCartUpdated;
1642          if( fu !== null ) {
1643            typeof  fu == 'function' ? fu(nG2.shoppingCart, NGY2Item.Get(G, ID)) : window[fu](nG2.shoppingCart, NGY2Item.Get(G, ID));
1644          }
1645          return nG2.shoppingCart;
1646          break;
1647          
1648        case 'shoppingCartRemove':
1649          if( typeof option === 'undefined' ){
1650            return false;
1651          }
1652          var ID=option;
1653          for( var i=0; i<nG2.shoppingCart.length; i++) {
1654            if( nG2.shoppingCart[i].ID=ID ) {
1655              nG2.shoppingCart.splice(i,1);
1656              break;
1657            }
1658          }
1659          var fu = G.O.fnShoppingCartUpdated;
1660          if( fu !== null ) {
1661            typeof fu == 'function' ? fu(nG2.shoppingCart, NGY2Item.Get(G, ID)) : window[fu](nG2.shoppingCart, NGY2Item.Get(G, ID));
1662          }
1663          return nG2.shoppingCart;
1664          break;
1665         
1666        case 'closeViewer':
1667          nG2.CloseViewer();
1668          break;
1669        case 'minimizeToolbar':
1670          nG2.MinimizeToolbar();
1671          break;
1672        case 'maximizeToolbar':
1673          nG2.MaximizeToolbar();
1674          break;
1675        case 'paginationPreviousPage':
1676          nG2.PaginationPreviousPage();
1677          break;
1678        case 'paginationNextPage':
1679          nG2.paginationNextPage();
1680          break;
1681        case 'paginationGotoPage':
1682          nG2.PaginationGotoPage( option );
1683          break;
1684        case 'paginationCountPages':
1685          nG2.PaginationCountPages();
1686          break;
1687         
1688      }
1689      return $(this);
1690
1691    }
1692  };
1693  
1694
1695  // ###############################
1696  // ##### nanogallery2 script #####
1697  // ###############################
1698
1699  /** @function nanoGALLERY2 */
1700  function nanoGALLERY2() {
1701    "use strict";
1702
1703    /**
1704    * Force reload the current album, if provided by Json
1705    */
1706    this.ReloadAlbum = function(){
1707      if( G.O.kind === '' ) {
1708        throw 'Not supported for this content source:' + G.O.kind;
1709      }
1710
1711      var albumIdx = G.GOM.albumIdx;
1712      if( albumIdx == -1 ) {
1713        throw ('Current album not found.');
1714      }
1715      
1716      var albumID = G.I[albumIdx].GetID();
1717
1718      // unselect everything & remove link to album (=logical delete)
1719      var l = G.I.length;
1720      for( var i = 0; i < l ; i++ ) {
1721        var item = G.I[i];
1722        if( item.albumID == albumID ) {
1723          item.selected = false;
1724        }
1725      }
1726      
1727      G.I[albumIdx].contentIsLoaded = false;
1728      
1729      DisplayAlbum('-1', albumID);
1730    };
1731    
1732    /**
1733     * Set one or several items selected/unselected
1734     * @param {array} items
1735     */
1736    this.ItemsSetSelectedValue = function(items, value){
1737      var l = items.length;
1738      for( var j = 0; j < l ; j++) {
1739        ThumbnailSelectionSet(items[j], value);
1740      }
1741    };
1742    
1743    /**
1744     * Returns an array of selected items
1745     * @returns {Array}
1746     */
1747    this.ItemsSelectedGet = function(){
1748      var selectedItems = [];
1749      var l = G.I.length;
1750      for( var i = 0; i < l ; i++ ) {
1751        if( G.I[i].selected == true ) {
1752          selectedItems.push(G.I[i]);
1753        }
1754      }
1755      return selectedItems;
1756    };    
1757    
1758    /**
1759     * Returns the value of an option
1760     * @param {string} option
1761     * @returns {nanoGALLERY.G.O}
1762     */
1763    this.Get = function(option){
1764        return G.O[option];
1765    };
1766
1767    /**
1768     * Set a new value for a defined option
1769     * @param {string} option
1770     */
1771    this.Set = function(option, value){
1772        G.O[option] = value;
1773        switch( option ) {
1774          case 'thumbnailSelectable':
1775            ThumbnailSelectionClear();
1776            // refresh the displayed gallery
1777            GalleryRender( G.GOM.albumIdx );
1778            break;
1779        }
1780    };
1781    
1782    /**
1783     * refresh the current gallery
1784     */
1785    this.Refresh = function() {
1786      // refresh the displayed gallery
1787      GalleryRender( G.GOM.albumIdx );
1788    };
1789    /**
1790     * resize the current gallery
1791     */
1792    this.Resize = function() {
1793      // resize the displayed gallery
1794      GalleryResize();
1795    };
1796
1797    /**
1798     * display one item (image or gallery)
1799     *   itemID syntax:
1800     *    - albumID --> display one album
1801     *    - albumID/imageID --> display one image
1802    */
1803    this.DisplayItem = function( itemID ) {
1804      var IDs=parseIDs( itemID );
1805      if( IDs.imageID != '0' ) {
1806        DisplayPhoto( IDs.imageID, IDs.albumID );
1807      }
1808      else {
1809        DisplayAlbum( '-1', IDs.albumID );
1810      }
1811    };
1812    
1813      
1814      
1815    var CountItemsToDisplay = function( gIdx ) {
1816      if( G.I[gIdx] == undefined ) { return 0; }
1817      var albumID = G.I[gIdx].GetID();
1818      var l = G.I.length;
1819      var cnt = 0;
1820      for( var idx = 0; idx < l; idx++ ) {
1821        var item = G.I[idx];
1822        if( item.isToDisplay(albumID) ) {
1823          cnt++;
1824        }
1825      }
1826      return cnt;
1827    } 
1828    /**
1829     * Search in the displayed gallery (in thumbnails title)
1830     */
1831    this.Search = function( search ) {
1832      G.GOM.albumSearch = search.toUpperCase();
1833      var gIdx = G.GOM.albumIdx;
1834      GalleryRender( G.GOM.albumIdx );
1835      return CountItemsToDisplay( gIdx );
1836    };
1837
1838    /**
1839     * Search2 in title and tags - set search values
1840     */
1841    this.Search2 = function( searchTitle, searchTags ) {
1842      if( searchTitle != null && searchTitle != undefined ) {
1843        G.GOM.albumSearch = searchTitle.toUpperCase();
1844      }
1845      else {
1846        G.GOM.albumSearch = '';
1847      }
1848      
1849      if( searchTags != null && searchTags != undefined ) {
1850        G.GOM.albumSearchTags = searchTags.toUpperCase();
1851      }
1852      else {
1853        G.GOM.albumSearchTags = '';
1854      }
1855      return CountItemsToDisplay( G.GOM.albumIdx );
1856    };
1857    
1858    /**
1859     * Search2 - execute the search on title and tags
1860     */
1861    this.Search2Execute = function() {
1862      var gIdx = G.GOM.albumIdx;
1863      GalleryRender( G.GOM.albumIdx );
1864      return CountItemsToDisplay( gIdx );
1865    };
1866    
1867    
1868    /**
1869     * Destroy the current gallery
1870     */
1871    this.Destroy = function(){
1872      // alert('destroy');
1873      // var event = new Event('build');
1874      if( G.GOM.hammertime != null ) {
1875        G.GOM.hammertime.destroy();
1876        G.GOM.hammertime = null;
1877      }
1878      // G.GOM.userEvents.RemoveEvtListener();
1879      // G.GOM.userEvents=null;
1880      // G.VOM.userEvents.RemoveEvtListener();
1881      // G.VOM.userEvents=null;
1882      if( G.VOM.hammertime != null ) {
1883        G.VOM.hammertime.destroy();
1884        G.VOM.hammertime = null;
1885      }
1886      //ThumbnailHoverReInitAll();  
1887      
1888      // color scheme
1889      $('#ngycs_' + G.baseEltID).remove()
1890      
1891      G.GOM.items = [];
1892      NGY2Item.New( G, G.i18nTranslations.breadcrumbHome, '', '0', '-1', 'album' );
1893      G.GOM.navigationBar.$newContent = null;
1894      G.$E.base.empty();
1895      G.$E.base.removeData();
1896      if( G.O.locationHash ) {
1897        jQuery(window).off('hashchange.nanogallery2.' + G.baseEltID);
1898      }
1899      jQuery(window).off('resize.nanogallery2.' + G.baseEltID);
1900      jQuery(window).off('orientationChange.nanogallery2.' + G.baseEltID);
1901      jQuery(window).off('scroll.nanogallery2.' + G.baseEltID);
1902      G.GOM.firstDisplay = false;
1903    };
1904    
1905    /**
1906     * CloseViewer - close the media viewer
1907     */
1908    this.CloseViewer = function() {
1909      CloseInternalViewer(null);
1910      return false;
1911    };
1912    
1913    /**
1914     * MinimizeToolbar - display the minimized lightbox main toolbar
1915     */
1916    this.MinimizeToolbar = function() {
1917      ViewerToolbarForVisibilityMin();
1918      return false;
1919    };
1920    
1921    /**
1922     * MaximizeToolbar - display the maximized/standard lightbox main toolbar
1923     */
1924    this.MaximizeToolbar = function() {
1925      ViewerToolbarForVisibilityStd();
1926      return false;
1927    };
1928    
1929    /**
1930     * PaginationPreviousPage - gallery paginate to previous page
1931     */
1932    this.PaginationPreviousPage = function() {
1933      paginationPreviousPage();
1934      return false;
1935    };
1936    
1937    
1938    /**
1939     * PaginationNextPage - gallery paginate to next page
1940     */
1941    this.PaginationNextPage = function() {
1942      paginationNextPage();
1943      return false;
1944    };
1945    
1946    
1947    /**
1948     * PaginationGotoPage - gallery paginate to specific page
1949     */
1950    this.PaginationGotoPage = function( page ) {
1951      var aIdx = G.$E.conPagin.data('galleryIdx');
1952      if( page > 1 ) { page--; }
1953      G.GOM.pagination.currentPage = page;
1954
1955      // scroll to top of gallery if not displayed
1956      if( !inViewportVert(G.$E.base, 0) ) {
1957        $('html, body').animate({scrollTop: G.$E.base.offset().top}, 200);
1958      }
1959      GalleryDisplayPart1( true );
1960      GalleryDisplayPart2( true );
1961      return false;
1962    };
1963
1964    /**
1965     * PaginationCountPages - gallery pagination - returns the number of pages
1966     */
1967    this.PaginationCountPages = function() {
1968      if( G.GOM.items.length == 0 ) { return 0; }   // no thumbnail to display
1969
1970      var nbPages = Math.ceil((G.GOM.items[G.GOM.items.length - 1].row + 1) / G.galleryMaxRows.Get());
1971      return nbPages;
1972    };
1973    
1974    
1975    
1976    
1977    // throttle()
1978    // author: underscore.js - http://underscorejs.org/docs/underscore.html
1979    // Returns a function, that, when invoked, will only be triggered at most once during a given window of time.
1980    // Normally, the throttled function will run as much as it can, without ever going more than once per wait duration;
1981    // but if you�d like to disable the execution on the leading edge, pass {leading: false}.
1982    // To disable execution on the trailing edge, ditto.
1983    var throttle = function(func, wait, options) {
1984      var context, args, result;
1985      var timeout = null;
1986      var previous = 0;
1987      if (!options) options = {};
1988      var later = function() {
1989        previous = options.leading === false ? 0 : new Date().getTime();
1990        timeout = null;
1991        result = func.apply(context, args);
1992        if (!timeout) context = args = null;
1993      };
1994      return function() {
1995        var now = new Date().getTime();
1996        if (!previous && options.leading === false) previous = now;
1997        var remaining = wait - (now - previous);
1998        context = this;
1999        args = arguments;
2000        if (remaining <= 0 || remaining > wait) {
2001          if (timeout) {
2002            clearTimeout(timeout);
2003            timeout = null;
2004          }
2005          previous = now;
2006          result = func.apply(context, args);
2007          if (!timeout) context = args = null;
2008        } else if (!timeout && options.trailing !== false) {
2009          timeout = setTimeout(later, remaining);
2010        }
2011        return result;
2012      };
2013    };
2014    
2015    
2016    // DEBOUNCE
2017    // author: John Hann - http://unscriptable.com/2009/03/20/debouncing-javascript-methods/
2018    // execAsap - false means executing at the end of the detection period
2019    var debounce = function (func, threshold, execAsap) {
2020      var timeout;
2021      return function debounced () {
2022          var obj = this, args = arguments;
2023          function delayed () {
2024              if (!execAsap)
2025                  func.apply(obj, args);
2026              timeout = null; 
2027          };
2028   
2029          if (timeout)
2030              clearTimeout(timeout);
2031              // clearRequestTimeout(timeout);
2032          else if (execAsap)
2033              func.apply(obj, args);
2034          timeout = setTimeout(delayed, threshold || 100); 
2035          // timeout = requestTimeout(delayed, threshold || 100); 
2036      };
2037    }
2038    
2039
2040    // Double requestAnimationFrame
2041    window.ng_draf = function (cb) {
2042      return requestAnimationFrame(function() {
2043        window.requestAnimationFrame(cb)
2044      })
2045    }    
2046    
2047    // REQUESTTIMEOUT - replace SETTIMEOUT - https://gist.github.com/joelambert/1002116
2048    /**
2049     * Behaves the same as setTimeout except uses requestAnimationFrame() where possible for better performance
2050     * @param {function} fn The callback function
2051     * @param {int} delay The delay in milliseconds
2052     */
2053
2054    window.requestTimeout = function(fn, delay) {
2055      // if( !window.requestAnimationFrame        && 
2056        // !window.webkitRequestAnimationFrame && 
2057        // !(window.mozRequestAnimationFrame && window.mozCancelRequestAnimationFrame) && // Firefox 5 ships without cancel support
2058        // !window.oRequestAnimationFrame      && 
2059        // !window.msRequestAnimationFrame)
2060          return window.setTimeout(fn, delay);
2061          
2062      var start = new Date().getTime(),
2063        handle = new Object();
2064        
2065      function loop(){
2066        var current = new Date().getTime(),
2067          delta = current - start;
2068  delta = delay;
2069  
2070        delta >= delay ? fn.call() : handle.value = requestAnimFrame(loop);
2071      };
2072      
2073      handle.value = requestAnimFrame(loop);
2074      return handle;
2075    };
2076
2077    
2078    // requestAnimationFrame() shim by Paul Irish
2079    // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
2080    window.requestAnimFrame = (function() {
2081      return  window.requestAnimationFrame       || 
2082          window.webkitRequestAnimationFrame || 
2083          window.mozRequestAnimationFrame    || 
2084          window.oRequestAnimationFrame      || 
2085          window.msRequestAnimationFrame     || 
2086          function(/* function */ callback, /* DOMElement */ element){
2087            window.setTimeout(callback, 1000 / 60);
2088          };
2089    })();    
2090    
2091    
2092    // CLEARREQUESTTIMEOUT - to replace CLEARTIMEOUT - https://gist.github.com/joelambert/1002116
2093    /**
2094     * Behaves the same as clearTimeout except uses cancelRequestAnimationFrame() where possible for better performance
2095     * @param {int|object} fn The callback function
2096     */
2097    window.clearRequestTimeout = function(handle) {
2098        window.cancelAnimationFrame ? window.cancelAnimationFrame(handle.value) :
2099        window.webkitCancelAnimationFrame ? window.webkitCancelAnimationFrame(handle.value) :
2100        window.webkitCancelRequestAnimationFrame ? window.webkitCancelRequestAnimationFrame(handle.value) : /* Support for legacy API */
2101        window.mozCancelRequestAnimationFrame ? window.mozCancelRequestAnimationFrame(handle.value) :
2102        window.oCancelRequestAnimationFrame ? window.oCancelRequestAnimationFrame(handle.value) :
2103        window.msCancelRequestAnimationFrame ? window.msCancelRequestAnimationFrame(handle.value) :
2104        clearTimeout(handle);
2105    };    
2106    
2107    
2108    
2109    /*
2110    ** Global data for this nanogallery2 instance
2111    **/
2112    var G=this;
2113    G.I =                       [];           // gallery items
2114    G.Id =                      [];           // gallery items
2115    G.O =                       null;         // user options
2116    G.baseEltID =               null;         // ID of the base element
2117    G.$E = { 
2118      base:                     null,         // base element
2119      conTnParent:              null,         // $g_containerThumbnailsParent
2120      conLoadingB:              null,         // loading bar - nanoGalleryLBarOff
2121      conConsole:               null,         // console for error messages
2122      conNavigationBar:         null,         // gallery navigation bar
2123      conTnBottom:              null         // container on the bottom of the gallery
2124    };
2125    G.shoppingCart =            [];
2126    G.layout = {                              // Layout informations
2127      internal :                true,
2128      engine :                  '',
2129      support :                 { rows: false },
2130      prerequisite :            { imageSize: false },
2131      SetEngine: function() {
2132
2133      if( G.layout.internal ) {
2134          if( G.tn.settings.width[G.GOM.curNavLevel][G.GOM.curWidth] == 'auto' || G.tn.settings.width[G.GOM.curNavLevel][G.GOM.curWidth] == '' ) {
2135            // do not use getH() / getW() here!
2136            G.layout.engine = 'JUSTIFIED';
2137            G.layout.support.rows = true;
2138            G.layout.prerequisite.imageSize = true;
2139            return;
2140          }
2141          if( G.tn.settings.height[G.GOM.curNavLevel][G.GOM.curWidth] == 'auto' || G.tn.settings.height[G.GOM.curNavLevel][G.GOM.curWidth] == '' ) {
2142            // do not use getH() / getW() here!
2143            G.layout.engine = 'CASCADING';
2144            G.layout.support.rows = false;
2145            G.layout.prerequisite.imageSize = true;
2146            return;
2147          }
2148
2149          if( G.tn.settings.getMosaic() != null ) {
2150            G.layout.engine = 'MOSAIC';
2151            G.layout.support.rows = true;
2152            G.layout.prerequisite.imageSize = false;
2153            return;
2154          }
2155          
2156          G.layout.engine='GRID';
2157          G.layout.support.rows=true;
2158          // if( G.tn.opt.Get('crop') === true ) {
2159            // G.layout.prerequisite.imageSize = true;
2160          // }
2161          // else {
2162            G.layout.prerequisite.imageSize = false;
2163          // }
2164        }
2165      }
2166    };
2167    G.galleryResizeEventEnabled = false;
2168    G.galleryMaxRows = { l1: 0, lN: 0,
2169      Get: function() {
2170        return G.galleryMaxRows[G.GOM.curNavLevel];
2171      }
2172    };
2173    G.galleryMaxItems = { l1: 0, lN: 0,
2174      Get: function() {
2175        return G.galleryMaxItems[G.GOM.curNavLevel];
2176      }
2177    };
2178    G.galleryFilterTags = { l1: 0, lN: 0,
2179      Get: function() {
2180        return G.galleryFilterTags[G.GOM.curNavLevel];
2181      }
2182    };
2183    G.galleryDisplayMode = { l1: 'FULLCONTENT', lN: 'FULLCONTENT',
2184      Get: function() {
2185        return G.galleryDisplayMode[G.GOM.curNavLevel];
2186      }
2187    };
2188    G.galleryLastRowFull = { l1: false, lN: false,
2189      Get: function() {
2190        return G.galleryLastRowFull[G.GOM.curNavLevel];
2191      }
2192    };
2193    G.gallerySorting = { l1: '', lN: '',
2194      Get: function() {
2195        return G.gallerySorting[G.GOM.curNavLevel];
2196      }
2197    };
2198    G.galleryDisplayTransition = { l1: 'none', lN: 'none',
2199      Get: function() {
2200        return G.galleryDisplayTransition[G.GOM.curNavLevel];
2201      }
2202    };
2203    G.galleryDisplayTransitionDuration = { l1: 500, lN: 500,
2204      Get: function() {
2205        return G.galleryDisplayTransitionDuration[G.GOM.curNavLevel];
2206      }
2207    };
2208    G.$currentTouchedThumbnail = null;    
2209    
2210    // ##### GENERAL THUMBNAILS PROPERTIES -->
2211    G.tn = {
2212      // levell specific options
2213      opt:  {
2214        l1: { crop: true, stacks: 0, stacksTranslateX: 0, stacksTranslateY: 0, stacksTranslateZ: 0, stacksRotateX: 0, stacksRotateY: 0, stacksRotateZ: 0, stacksScale: 0, gutterHeight: 0, gutterWidth: 0, baseGridHeight: 0, displayTransition: 'FADEIN', displayTransitionStartVal: 0, displayTransitionEasing: 'easeOutQuart', displayTransitionDuration: 240, displayInterval: 15 },
2215        lN: { crop: true, stacks: 0, stacksTranslateX: 0, stacksTranslateY: 0, stacksTranslateZ: 0, stacksRotateX: 0, stacksRotateY: 0, stacksRotateZ: 0, stacksScale: 0, gutterHeight: 0, gutterWidth: 0, baseGridHeight: 0, displayTransition: 'FADEIN', displayTransitionStartVal: 0, displayTransitionEasing: 'easeOutQuart', displayTransitionDuration: 240, displayInterval: 15 },
2216        Get: function(opt) {
2217          return G.tn.opt[G.GOM.curNavLevel][opt];
2218        }
2219      },
2220      scale:                          1,         // image scale depending of the hover effect
2221      borderWidth:                    0,         // thumbnail container border width
2222      borderHeight:                   0,         // thumbnail container border height
2223      labelHeight: {                  // in case label on bottom, otherwise always=0
2224        l1: 0, lN: 0,
2225        get: function() {
2226          return G.tn.labelHeight[G.GOM.curNavLevel];
2227        }
2228      },
2229      defaultSize: {                  // default thumbnail size
2230                                      // label height is not included
2231        width: {  l1 : { xs:0, sm:0, me:0, la:0, xl:0 }, lN : { xs:0, sm:0, me:0, la:0, xl:0 } },
2232        height: { l1 : { xs:0, sm:0, me:0, la:0, xl:0 }, lN : { xs:0, sm:0, me:0, la:0, xl:0 } },
2233        getWidth: function() {
2234          return G.tn.defaultSize.width[G.GOM.curNavLevel][G.GOM.curWidth];
2235        },
2236        getOuterWidth: function() {     // width border included
2237          return G.tn.defaultSize.width[G.GOM.curNavLevel][G.GOM.curWidth]+G.tn.borderWidth*2;
2238        },
2239        getHeight: function() {
2240          return G.tn.defaultSize.height[G.GOM.curNavLevel][G.GOM.curWidth];
2241        },
2242        getOuterHeight: function() {     // height, border included
2243          return G.tn.defaultSize.height[G.GOM.curNavLevel][G.GOM.curWidth]+G.tn.borderHeight*2;
2244        }
2245      },
2246      settings: {                     // user defined width/height of the image to display depending on the screen size
2247        width: {  l1 : { xs:0, sm:0, me:0, la:0, xl:0, xsc:'u', smc:'u', mec:'u', lac:'u', xlc:'u' },
2248                  lN : { xs:0, sm:0, me:0, la:0, xl:0, xsc:'u', smc:'u', mec:'u', lac:'u', xlc:'u' } },
2249        height: { l1 : { xs:0, sm:0, me:0, la:0, xl:0, xsc:'u', smc:'u', mec:'u', lac:'u', xlc:'u' }, 
2250                  lN : { xs:0, sm:0, me:0, la:0, xl:0, xsc:'u', smc:'u', mec:'u', lac:'u', xlc:'u' } },
2251        getH: function(l, w) {
2252          var cl = (l == undefined ? G.GOM.curNavLevel : l);
2253          var cw = (w == undefined ? G.GOM.curWidth : w);
2254          if( G.layout.engine == 'MOSAIC' ) {
2255          return G.tn.settings.height[cl][cw] * G.tn.settings.mosaic[cl+'Factor']['h'][cw];
2256          }
2257          else {
2258            return G.tn.settings.height[cl][cw];
2259          }
2260        },
2261        getW: function(l, w) {
2262          var cl = (l == undefined ? G.GOM.curNavLevel : l);
2263          var cw = (w == undefined ? G.GOM.curWidth : w);
2264          if( G.layout.engine == 'MOSAIC' ) {
2265            return G.tn.settings.width[cl][cw] * G.tn.settings.mosaic[cl+'Factor']['w'][cw];
2266          }
2267          else {
2268            return G.tn.settings.width[cl][cw];
2269            // return G.tn.settings.width[G.GOM.curNavLevel][G.GOM.curWidth];
2270          }
2271        },
2272        mosaic: { l1 : { xs: null, sm: null, me: null, la: null, xl: null },
2273                  lN : { xs: null, sm: null, me: null, la: null, xl: null },
2274                  l1Factor : { h :{ xs: 1, sm: 1, me: 1, la: 1, xl: 1 }, w :{ xs: 1, sm: 1, me: 1, la: 1, xl: 1 }},
2275                  lNFactor : { h :{ xs: 1, sm: 1, me: 1, la: 1, xl: 1 }, w :{ xs: 1, sm: 1, me: 1, la: 1, xl: 1 }}
2276                  },
2277        getMosaic: function() {
2278          return G.tn.settings.mosaic[G.GOM.curNavLevel][G.GOM.curWidth];
2279        },
2280        mosaicCalcFactor: function(l, w) {
2281            // retrieve max size multiplicator
2282            var maxW = 1;
2283            var maxH = 1;
2284            for( var n = 0; n < G.tn.settings.mosaic[l][w].length; n++ ) {
2285              maxW = Math.max(maxW, G.tn.settings.mosaic[l][w][n]['w']);
2286              maxH = Math.max(maxH, G.tn.settings.mosaic[l][w][n]['h']);
2287            }
2288            G.tn.settings.mosaic[l + 'Factor']['h'][w] = maxH;
2289            G.tn.settings.mosaic[l + 'Factor']['w'][w] = maxW;
2290        }
2291      },
2292      // thumbnail hover effects
2293      hoverEffects : {
2294        std :   [],
2295        level1: [],
2296        get: function() {
2297          if( G.GOM.curNavLevel == 'l1' && G.tn.hoverEffects.level1.length !== 0 ) {
2298            return G.tn.hoverEffects.level1;
2299          }
2300          else {
2301            return G.tn.hoverEffects.std;
2302          }
2303        }
2304      },
2305      // thumbnail init
2306      buildInit : {
2307        std :   [],
2308        level1: [],
2309        get: function() {
2310          if( G.GOM.curNavLevel == 'l1' && G.tn.buildInit.level1.length !== 0 ) {
2311            return G.tn.buildInit.level1;
2312          }
2313          else {
2314            return G.tn.buildInit.std;
2315          }
2316        }
2317      },
2318      // thumbnail toolbars
2319      toolbar: {
2320        album :   { topLeft : '', topRight: '', bottomLeft: '', bottomRight: '' },
2321        image :   { topLeft : '', topRight: '', bottomLeft: '', bottomRight: '' },
2322        albumUp : { topLeft : '', topRight: '', bottomLeft: '', bottomRight: '' },
2323        get: function( item ) {
2324          return G.tn.toolbar[item.kind];
2325        },
2326      },
2327      style: {
2328        // inline CSS
2329        l1 : { label: '', title: '', desc: '' },
2330        lN : { label: '', title: '', desc: '' },
2331        getTitle : function() {
2332          return ('style="' + G.tn.style[G.GOM.curNavLevel].title + '"');
2333        },
2334        getDesc : function() {
2335          return ('style="' + G.tn.style[G.GOM.curNavLevel].desc + '"');
2336        },
2337        getLabel: function() {
2338          var s='style="'+ G.tn.style[G.GOM.curNavLevel].label;
2339          s+= (G.O.RTL ? '"direction:RTL;"' :'');
2340          s+='"';
2341          return s;
2342        }
2343      }
2344    };
2345    G.scrollTimeOut =             0;
2346    G.i18nTranslations =          {'paginationPrevious':'Previous', 'paginationNext':'Next', 'breadcrumbHome':'List of Albums', 'thumbnailImageTitle':'', 'thumbnailAlbumTitle':'', 'thumbnailImageDescription':'', 'thumbnailAlbumDescription':'' };
2347    G.emptyGif =                  'data:image/gif;base64,R0lGODlhEAAQAIAAAP///////yH5BAEKAAEALAAAAAAQABAAAAIOjI+py+0Po5y02ouzPgUAOw==';
2348    G.CSStransformName =          FirstSupportedPropertyName(["transform", "msTransform", "MozTransform", "WebkitTransform", "OTransform"]);
2349    // G.CSSfilterName =          FirstSupportedPropertyName(["filter", "WebkitFilter"]);
2350    G.CSStransformStyle =         FirstSupportedPropertyName(["transformStyle", "msTransformStyle", "MozTransformStyle", "WebkitTransformStyle", "OTransformStyle"]);
2351    G.CSSperspective =            FirstSupportedPropertyName(["perspective", "msPerspective", "MozPerspective", "WebkitPerspective", "OPerspective"]);
2352    G.CSSbackfaceVisibilityName = FirstSupportedPropertyName(["backfaceVisibility", "msBackfaceVisibility", "MozBackfaceVisibility", "WebkitBackfaceVisibility", "OBackfaceVisibility"]);
2353    G.CSStransitionName =         FirstSupportedPropertyName(["transition", "msTransition", "MozTransition", "WebkitTransition", "OTransition"]);
2354    G.CSSanimationName =          FirstSupportedPropertyName(["animation", "msAnimation", "MozAnimation", "WebkitAnimation", "OAnimation"]);
2355    G.GalleryResizeThrottled =    throttle(GalleryResize, 30, {leading: false});
2356    
2357    G.blackList =                 null;     // album white list
2358    G.whiteList =                 null;     // album black list
2359    G.albumList =                 [];       // album list
2360    G.locationHashLastUsed =      '';
2361    G.custGlobals =               {};
2362    G.touchAutoOpenDelayTimerID = 0;
2363    G.i18nLang =                  '';
2364    G.timeLastTouchStart =        0;
2365    G.custGlobals =               {};
2366    G.markupOrApiProcessed =      false;
2367    
2368    //------------------------
2369    //--- Gallery Object Model
2370    G.GOM = {
2371      albumIdx :                  -1, // index (in G.I) of the currently displayed album
2372      clipArea :                  { top: 0, height: 0 }, // area of the GOM to display on screen
2373      displayArea :               { width: 0 , height: 0 }, // size of the GOM area (=used area, not available area)
2374      displayAreaLast :           { width: 0 , height: 0 }, // previous size of the GOM area
2375      displayedMoreSteps :        0,  // current number of displayed steps (moreButton mode)
2376      items:                      [], // current items of the GOMS
2377      $imgPreloader:              [],
2378      thumbnails2Display:         [],
2379      itemsDisplayed :            0, // number of currently displayed thumbnails
2380      firstDisplay :              true,
2381      firstDisplayTime :          0,      // in conjunction with galleryRenderDelay
2382      navigationBar : {           // content of the navigation bar (for breadcrumb and filter tags)
2383        displayed:                false,
2384        $newContent:              ''
2385      },
2386      cache : {                   // cached data
2387        viewport:                 null,
2388        containerOffset:          null,
2389        areaWidth:                100         // available area width
2390      },
2391      nbSelected :                0,        // number of selected items
2392      pagination :                { currentPage: 0 }, // pagination data
2393      panThreshold:               60,       // threshold value (in pixels) to block horizontal pan/swipe
2394      panYOnly:                   false,    // threshold value reach -> definitively block horizontal pan until end of pan
2395      lastFullRow :               -1,       // number of the last row without holes
2396      lastDisplayedIdx:           -1,       // used to display the counter of not displayed items
2397      displayInterval :           { from: 0, len: 0 },
2398      userEvents:                 null,
2399      hammertime:                 null,
2400      curNavLevel:                'l1',   // current navigation level (l1 or LN)
2401      curWidth:                   'me',
2402      albumSearch:                '',     // current search string -> title (used to filter the thumbnails on screen)
2403      albumSearchTags:            '',     // current search string -> tags
2404      lastZIndex:                 0,      // used to put a thumbnail on top of all others (for exemple for scale hover effect)
2405      lastRandomValue:            0,
2406      slider : {                          // slider on last thumbnail
2407        hostIdx:                  -1,       // idx of the thumbnail hosting the slider
2408        hostItem:                 null,     // item hosting the slider
2409        currentIdx:               0,        // idx of the current displayed item
2410        nextIdx:                  0,        // idx of the next item to display in the slider
2411        timerID:                  0,
2412        tween:                    null      // tranistion tween instance
2413      },
2414      NGY2Item: function( idx ) {   // returns a NGY2Item or null if it does not exist
2415        if( G.GOM.items[idx] == undefined || G.GOM.items[idx] == null ) { return null; }
2416        var i = G.GOM.items[idx].thumbnailIdx;
2417        return G.I[i];
2418      },
2419      // One GOM item (thumbnail)
2420      // function GTn(index, width, height) {
2421      GTn: function(index, width, height) {
2422        this.thumbnailIdx = index;
2423        this.width =                0;      // thumbnail width
2424        this.height =               0;      // thumbnail height
2425        this.top =                  0;      // position: top
2426        this.left =                 0;      // position: left
2427        this.row =                  0;      // position: row number
2428        this.imageWidth =           width;  // image width
2429        this.imageHeight =          height; // image height
2430        this.resizedContentWidth =  0;
2431        this.resizedContentHeight = 0;
2432        this.displayed =            false;
2433        this.neverDisplayed =       true;
2434        this.inDisplayArea =        false;
2435      }
2436    };
2437    
2438    
2439    //------------------------
2440    //--- Viewer Object Model
2441    
2442    G.VOM = {
2443      viewerDisplayed:            false,  // is the viewer currently displayed
2444      viewerIsFullscreen:         false,  // viewer in fullscreen mode
2445      infoDisplayed:              false,  // is the info box displayed
2446      toolbarsDisplayed:          true,   // the toolbars are displayed
2447      toolsHide:                  null,
2448      saveOverflowX:              'visible',    // store the value to restore it back after viewer is closed
2449      saveOverflowY:              'visible',
2450      zoom : {
2451        posX:                     0,      // position to center zoom in/out
2452        posY:                     0,
2453        userFactor:               1,      // user zoom factor (applied to the baseZoom factor)
2454        isZooming:                false
2455      },
2456      padding:                    { H: 0, V: 0 }, // padding for the image
2457      window:                     { lastWidth: 0, lastHeight: 0 },
2458      $cont:                      null,   // viewer container
2459      $viewer:                    null,
2460      $toolbar:                   null,   // viewerToolbar
2461      $toolbarTL:                 null,   // viewer toolbar on top left
2462      $toolbarTR:                 null,   // viewer toolbar on top right
2463      $content:                   null,   // viewer content
2464      
2465      $mediaPrevious:             null,   // previous image
2466      $mediaCurrent:              null,   // current image
2467      $mediaNext:                 null,   // next image
2468      toolbarMode:                'std',  // current toolbar mode (standard, minimized)
2469      playSlideshow :             false,  // slide show mode status
2470      playSlideshowTimerID:       0,      // slideshow mode time
2471      slideshowDelay:             3000,   // slideshow mode - delay before next image
2472      albumID:                    -1,
2473      currItemIdx:                -1,
2474      viewerMediaIsChanged:       false,  // media display is currently modified
2475      items:                      [],     // current list of images to be managed by the viewer
2476      NGY2Item: function( n ) {   // returns a NGY2Item
2477        switch( n ) {
2478          case -1:   // previous
2479            var idx=this.IdxPrevious();
2480            return G.I[this.items[idx].ngy2ItemIdx];
2481            break;
2482          case 1:   // next
2483            var idx=this.IdxNext();
2484            return G.I[this.items[idx].ngy2ItemIdx];
2485            break;
2486          case 0:   // current
2487          default:
2488            return G.I[this.items[G.VOM.currItemIdx].ngy2ItemIdx];
2489            break;
2490        }
2491      },
2492      IdxNext: function() {
2493        var n = 0;
2494        if( G.VOM.currItemIdx != (G.VOM.items.length-1) ) {
2495          n = G.VOM.currItemIdx + 1;
2496        }
2497        return n;
2498      },
2499      IdxPrevious: function() {
2500        var n = G.VOM.currItemIdx - 1;
2501        if( G.VOM.currItemIdx == 0 ) {
2502          n = G.VOM.items.length - 1;
2503        }
2504        return n;
2505      },
2506      userEvents:         null,   // user events management
2507      hammertime:         null,   // hammer.js manager
2508      swipePosX:          0,      // current horizontal swip position
2509      panPosX:            0,      // position for manual pan
2510      panPosY:            0,
2511      panThreshold:       60,     // threshold value (in pixels) to block vertical pan
2512      panXOnly:           false,  // threshold value reach -> definitively block vertical pan until end of pan
2513      viewerTheme:        '',
2514      timeImgChanged:     0,
2515      ImageLoader: {
2516        // fires a callback when image size is know (during download)
2517        // inspired by ROB - http://stackoverflow.com/users/226507/rob
2518        maxChecks:        1000,
2519        list:             [],
2520        intervalHandle :  null,
2521
2522        loadImage : function (callback, ngitem) {
2523          if( ngitem.mediaKind != 'img' ) { return; }     // ignore - only for images
2524          var img = new Image ();
2525          img.src = ngitem.responsiveURL();
2526          if (img.width && img.height) {
2527            callback (img.width, img.height, ngitem, 0);
2528            }
2529          else {
2530            var obj = {image: img, url: ngitem.responsiveURL(), ngitem: ngitem, callback: callback, checks: 1};
2531            var i;
2532            for (i=0; i < this.list.length; i++)    {
2533              if (this.list[i] == null)
2534                break;
2535              }
2536            this.list[i] = obj;
2537            if (!this.intervalHandle)
2538              this.intervalHandle = setInterval(this.interval, 50);
2539            }
2540          },
2541
2542        // called by setInterval  
2543        interval : function () {
2544          var count = 0;
2545          var list = G.VOM.ImageLoader.list, item;
2546          for (var i=0; i<list.length; i++) {
2547            item = list[i];
2548            if (item != null) {
2549              if (item.image.width && item.image.height) {
2550                G.VOM.ImageLoader.list[i] = null;
2551                item.callback (item.image.width, item.image.height, item.ngitem, item.checks);
2552                }
2553              else if (item.checks > G.VOM.ImageLoader.maxChecks) {
2554                G.VOM.ImageLoader.list[i] = null;
2555                item.callback (0, 0, item.ngitem, item.checks);
2556                }
2557              else {
2558                count++;
2559                item.checks++;
2560                }
2561              }
2562            }
2563          if (count == 0) {
2564            G.VOM.ImageLoader.list = [];
2565            clearInterval (G.VOM.ImageLoader.intervalHandle);
2566            delete G.VOM.ImageLoader.intervalHandle;
2567            }
2568          }
2569        }
2570    }
2571    // One VOM item (image)
2572    function VImg(index) {
2573      this.$e = null;
2574      this.ngy2ItemIdx = index;
2575      this.mediaNumber = 0;     
2576      this.posX = 0;    // to center the element
2577      this.posY = 0;
2578    }
2579    
2580    
2581    //------------------------
2582    //--- popup
2583    G.popup = {
2584      isDisplayed:      false,
2585      $elt:             null,
2586      close: function() {
2587        if( this.$elt != null ) {
2588          var tweenable = new NGTweenable();
2589          tweenable.tween({
2590            from:       { opacity:1  },
2591            to:         { opacity:0 },
2592            attachment: { t: this },
2593            easing:     'easeInOutSine',
2594            duration:   100,
2595            step: function (state, att) {
2596              if( att.t.$elt != null ) {
2597                att.t.$elt.css('opacity',state.opacity);
2598              }
2599            },
2600            finish: function (state, att) {
2601              if( att.t.$elt != null ) {
2602                att.t.$elt.remove();
2603                att.t.$elt=null;
2604              }
2605              att.t.isDisplayed=false;
2606            }
2607          });
2608        }
2609      }
2610    }
2611    
2612    
2613    // Color schemes - Gallery
2614    // gadrient generator: https://www.grabient.com/
2615    G.galleryTheme_dark = {
2616      navigationBar :         { background: 'none', borderTop: '', borderBottom: '', borderRight: '', borderLeft: '' },
2617      navigationBreadcrumb :  { background: '#111', color: '#fff', colorHover: '#ccc', borderRadius: '4px' },
2618      navigationFilter :      { color: '#ddd', background: '#111', colorSelected: '#fff', backgroundSelected: '#111', borderRadius: '4px' },
2619      thumbnail :             { background: '#444', backgroundImage: 'linear-gradient(315deg, #111 0%, #445 90%)', borderColor: '#000', labelOpacity : 1, labelBackgroun
2619d: 'rgba(34, 34, 34, 0)', titleColor: '#fff', titleBgColor: 'transparent', titleShadow: '', descriptionColor: '#ccc', descriptionBgColor: 'transparent', descriptionShadow: '', stackBackground: '#aaa' },
2620      thumbnailIcon :         { padding: '5px', color: '#fff' },
2621      pagination :            { background: '#181818', backgroundSelected: '#666', color: '#fff', borderRadius: '2px', shapeBorder: '3px solid #666', shapeColor: '#444', shapeSelectedColor: '#aaa'}
2622    };
2623
2624    G.galleryTheme_light = {
2625      navigationBar :         { background: 'none', borderTop: '', borderBottom: '', borderRight: '', borderLeft: '' },
2626      navigationBreadcrumb :  { background: '#eee', color: '#000', colorHover: '#333', borderRadius: '4px' },
2627      navigationFilter :      { background: '#eee', color: '#222', colorSelected: '#000', backgroundSelected: '#eee', borderRadius: '4px' },
2628      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' },
2629      thumbnailIcon :         { padding: '5px', color: '#fff' },
2630      pagination :            { background: '#eee', backgroundSelected: '#aaa', color: '#000', borderRadius: '2px', shapeBorder: '3px solid #666', shapeColor: '#444', shapeSelectedColor: '#aaa'}
2631    };
2632
2633    // Color schemes - lightbox
2634    G.viewerTheme_dark = {
2635      background:             '#000',
2636      imageBorder:            'none',
2637      imageBoxShadow:         'none',
2638      barBackground:          'rgba(4, 4, 4, 0.2)',
2639      barBorder:              '0px solid #111',
2640      barColor:               '#eee',
2641      barDescriptionColor:    '#aaa'
2642    };
2643    G.viewerTheme_border = {
2644      background:             'rgba(1, 1, 1, 0.75)',
2645      imageBorder:            '4px solid #f8f8f8',
2646      imageBoxShadow:         '#888 0px 0px 20px',
2647      barBackground:          'rgba(4, 4, 4, 0.2)',
2648      barBorder:              '0px solid #111',
2649      barColor:               '#eee',
2650      barDescriptionColor:    '#aaa'
2651    };
2652    G.viewerTheme_light = {
2653      background:             '#f8f8f8',
2654      imageBorder:            'none',
2655      imageBoxShadow:         'none',
2656      barBackground:          'rgba(4, 4, 4, 0.7)',
2657      barBorder:              '0px solid #111',
2658      barColor:               '#eee',
2659      barDescriptionColor:    '#aaa'
2660    };
2661
2662      
2663    
2664    // shortcut with G context to NGY2TOOLS
2665    // var NanoAlert =           NGY2Tools.NanoAlert.bind(G);
2666    // var NanoConsoleLog =      NGY2Tools.NanoConsoleLog.bind(G);
2667    var NanoAlert =           NGY2Tools.NanoAlert;
2668    var NanoConsoleLog =      NGY2Tools.NanoConsoleLog;
2669
2670    
2671    /** @function initiateGallery2 */
2672    this.initiateGallery2 = function( element, params ) {
2673
2674      // GLOBAL OPTIONS
2675      G.O =               params;
2676      // Base element
2677      G.$E.base =         jQuery(element);
2678      G.baseEltID =       G.$E.base.attr('id');
2679      if( G.baseEltID == undefined ) {
2680        // set a default ID to the root container
2681        G.baseEltID='my_nanogallery';
2682        G.$E.base.attr('id', G.baseEltID)
2683      }
2684      G.O.$markup =       [];
2685      DefineVariables();
2686      SetPolyFills();
2687      BuildSkeleton();
2688      G.GOM.firstDisplayTime=Date.now();
2689      
2690      SetGlobalEvents();
2691
2692      // check if only one specific album will be used
2693      var albumToDisplay = G.O.album;
2694      if( albumToDisplay == '' && G.O.photoset != '' ) {
2695        albumToDisplay = G.O.photoset;
2696        G.O.album = G.O.photoset;
2697      }
2698      if( albumToDisplay != '' ) {
2699        G.O.displayBreadcrumb = false;    // no breadcrumb since only 1 album
2700        if( albumToDisplay.toUpperCase() != 'NONE' ) {
2701          // open specific album
2702        
2703          var p=albumToDisplay.indexOf('&authkey=');
2704          if( p == -1 ) {
2705            p=albumToDisplay.indexOf('?authkey=');
2706          }
2707          if( p > 0 ) {
2708            // privat album with authkey
2709            G.O.locationHash=false;   // disable hash location for hidden/privat albums --> impossible to handle
2710            var albumID=albumToDisplay.substring(0,p);
2711            var opt=albumToDisplay.substring(p);
2712            if( opt.indexOf('Gv1sRg') == -1 ) {
2713              opt = '&authkey=Gv1sRg'+opt.substring(9);
2714            }
2715            var newItem = NGY2Item.New( G, '', '', albumID, '-1', 'album' );
2716            newItem.authkey = opt;
2717            DisplayAlbum('-1', albumID);
2718          }
2719          else {
2720            // open a public album
2721            if( G.O.kind == "nano_photos_provider2") {
vendor: 141,104 bytes, lines 2722-6413
2722              if( albumToDisplay == decodeURIComponent(albumToDisplay)) {
2723                // album ID must be encoded
2724                albumToDisplay = encodeURIComponent(albumToDisplay);
2725                G.O.album = albumToDisplay;
2726              }
2727            }
2728            NGY2Item.New( G, '', '', albumToDisplay, '-1', 'album' );
2729            if( !ProcessLocationHash() ) {
2730              DisplayAlbum('-1', albumToDisplay);
2731            }
2732          }
2733          return;
2734        }
2735      }
2736      
2737      // use full content
2738      // add base album
2739      NGY2Item.New( G, G.i18nTranslations.breadcrumbHome, '', '0', '-1', 'album' );
2740
2741      processStartOptions();
2742
2743    }
2744
2745
2746    /** @function processStartOptions */
2747    function processStartOptions() {
2748      // open image or album
2749      // 1. load hidden albums
2750      // 2. check if location hash set (deep linking)
2751      // 3. check openOnStart parameter
2752      // 4. open root album (ID=-1)
2753
2754      // hidden/private albums are loaded on plugin start (Picasa) --> no more available in Google Photos
2755      // if( G.albumListHidden.length > 0 ) {
2756      //  jQuery.nanogallery2['data_'+G.O.kind](G, 'GetHiddenAlbums', G.albumListHidden, processStartOptionsPart2);
2757      //  return;
2758      //}
2759      
2760      if( !ProcessLocationHash() ) {
2761        processStartOptionsPart2();
2762      }
2763    }
2764
2765    /** @function processStartOptionsPart2 */
2766    function processStartOptionsPart2() {
2767  
2768      // Check location hash + start parameters -> determine what to do on start
2769      // openOnStart parameter
2770      if( G.O.openOnStart != '' ) {
2771        var IDs = parseIDs(G.O.openOnStart);
2772        if( IDs.imageID != '0' ) {
2773          DisplayPhoto(IDs.imageID, IDs.albumID);
2774        }
2775        else {
2776          DisplayAlbum('-1', IDs.albumID);
2777        }
2778      }
2779      else {
2780        // open root album (ID = -1)
2781        DisplayAlbum('-1', 0);
2782      }
2783    }
2784    
2785    // Parse string to extract albumID and imageID (format albumID/imageID)
2786    function parseIDs( IDs ) {
2787      var r={ albumID: '0', imageID: '0' };
2788      
2789      var t=IDs.split('/');
2790      if( t.length > 0 ) {
2791        r.albumID = t[0];
2792        if( t.length > 1 ) {
2793          r.imageID = t[1];
2794        }
2795      }
2796      return r;
2797    }
2798    
2799
2800    /** @function DisplayAlbum */
2801    function DisplayAlbum( imageID, albumID ) {
2802      // close viewer if already displayed
2803      if( G.VOM.viewerDisplayed ) {
2804        CloseInternalViewer(null);
2805      }
2806    
2807      // set current navigation level (l1 or lN)
2808      var albumIdx = NGY2Item.GetIdx(G, albumID);
2809      G.GOM.curNavLevel = 'lN';
2810      if( albumIdx == 0 ) {
2811        G.GOM.curNavLevel = 'l1';
2812      }
2813      G.layout.SetEngine();
2814      G.galleryResizeEventEnabled = false;
2815
2816      if( albumIdx == -1 ) {
2817        NGY2Item.New( G, '', '', albumID, '0', 'album' );    // create empty album
2818        albumIdx = G.I.length - 1;
2819      }
2820    
2821      if( !G.I[albumIdx].contentIsLoaded ) {
2822        // get content of the album if not already loaded
2823        AlbumGetContent( albumID, DisplayAlbum, imageID, albumID );
2824        return;
2825      }
2826    
2827      ThumbnailSelectionClear();
2828    
2829      G.GOM.pagination.currentPage = 0;
2830      SetLocationHash( albumID, '' );
2831      GalleryRender( albumIdx );
2832    
2833    }
2834
2835
2836    //----- manage the bottom area of the gallery -> "pagination" or "more button"
2837    function GalleryBottomManage() {
2838
2839      switch( G.galleryDisplayMode.Get() ) {
2840        case 'PAGINATION':
2841          if( G.layout.support.rows && G.galleryMaxRows.Get() > 0 ) {
2842            ManagePagination( G.GOM.albumIdx );
2843          }
2844          break;
2845        case 'MOREBUTTON':
2846          G.$E.conTnBottom.off('click');
2847          var nb = G.GOM.items.length-G.GOM.itemsDisplayed;
2848          if( nb == 0 ) {
2849            G.$E.conTnBottom.empty();
2850          }
2851          else {
2852            G.$E.conTnBottom.html('<div class="nGY2GalleryMoreButton"><div class="nGY2GalleryMoreButtonAnnotation">+'+nb+' ' + G.O.icons.galleryMoreButton +'</div></div>');
2853            G.$E.conTnBottom.on('click', function(e) {
2854              G.GOM.displayedMoreSteps++;
2855              GalleryResize();
2856            });
2857          }
2858          break;
2859        case 'FULLCONTENT':
2860        default:
2861          break;
2862      }
2863    }
2864    
2865    
2866    // add one album/folder to the breadcrumb
2867    function breadcrumbAdd( albumIdx ) {
2868
2869      var ic='';
2870      if( !G.O.breadcrumbHideIcons ) {
2871        ic=G.O.icons.breadcrumbAlbum;
2872        if( albumIdx == 0 ) {
2873          ic=G.O.icons.breadcrumbHome;
2874        }
2875      }
2876      var $newDiv =jQuery('<div class="oneItem">'+ic + G.I[albumIdx].title+'</div>').appendTo(G.GOM.navigationBar.$newContent.find('.nGY2Breadcrumb'));
2877      if( G.O.breadcrumbOnlyCurrentLevel ) {
2878        // link to parent folder (only 1 level is displayed in the breadcrumb)
2879        if( albumIdx == 0 ) {
2880          // no parent level -> stay on current one
2881          jQuery($newDiv).data('albumID','0');
2882        }
2883        else {
2884          jQuery($newDiv).data('albumID',G.I[albumIdx].albumID);
2885        }
2886      }
2887      else {
2888        // link to current folder
2889        jQuery($newDiv).data('albumID',G.I[albumIdx].GetID());
2890      }
2891      $newDiv.click(function() {
2892        var cAlbumID=jQuery(this).data('albumID');
2893        DisplayAlbum('-1', cAlbumID);
2894        return;
2895      });
2896    }
2897
2898    // add one separator to breadcrumb
2899    function breadcrumbAddSeparator( lastAlbumID ) {
2900      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'));
2901      jQuery($newSep).data('albumIdx',lastAlbumID);
2902      $newSep.click(function() {
2903        var sepAlbumIdx=jQuery(this).data('albumIdx');
2904        DisplayAlbum('-1', G.I[sepAlbumIdx].GetID());
2905        return;
2906      });
2907    }
2908
2909    
2910    
2911    // Manage the gallery toolbar (breadcrumb + tag filter)
2912    function GalleryNavigationBar( albumIdx ) {
2913
2914      // Title + background image
2915      // var bgImage='';
2916      // var l=G.I.length;
2917      // var albumID = G.I[albumIdx].GetID();
2918      // for( var idx=0; idx<l ; idx++) {
2919        // var item=G.I[idx];
2920        // if( item.kind == 'image' && item.isToDisplay(albumID) ) {
2921          // 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>';
2922          // break;
2923        // }
2924      // }
2925
2926      //console.log(bgImage);
2927    
2928      // new navigation bar items are not build in the DOM, but in memory
2929      G.GOM.navigationBar.$newContent=jQuery('<div class="nGY2Navigationbar"></div>');
2930      //G.GOM.navigationBar.$newContent = jQuery(bgImage );
2931      //console.log(G.GOM.navigationBar.$newContent);
2932
2933      //-- manage breadcrumb
2934      if( G.O.displayBreadcrumb == true && !G.O.thumbnailAlbumDisplayImage) {
2935        // retrieve new folder level
2936        var newLevel = 0,
2937        lstItems=[];
2938        if( albumIdx != 0 ) {
2939          var l=G.I.length,
2940          parentID=0;
2941          
2942          lstItems.push(albumIdx);
2943          var curIdx=albumIdx;
2944          newLevel++;
2945          
2946          while( G.I[curIdx].albumID != 0 && G.I[curIdx].albumID != -1) {
2947            for(var i=1; i < l; i++ ) {
2948              if( G.I[i].GetID() == G.I[curIdx].albumID ) {
2949                curIdx=i;
2950                lstItems.push(curIdx);
2951                newLevel++;
2952                break;
2953              }
2954            }
2955          }
2956        }
2957        
2958        // build breadcrumb
2959        if( !(G.O.breadcrumbAutoHideTopLevel && newLevel == 0) ) {
2960          BreadcrumbBuild( lstItems );
2961        }
2962      }
2963
2964      
2965      //-- manage and build tag filters
2966      if( G.galleryFilterTags.Get() != false ) {
2967        var nTags=G.I[albumIdx].albumTagList.length;
2968        if( nTags > 0 ) {
2969          for(var i=0; i < nTags; i++ ) {
2970            var s=G.I[albumIdx].albumTagList[i];
2971            var ic=G.O.icons.navigationFilterUnselected;
2972            var tagClass='Unselected';
2973            if( jQuery.inArray(s, G.I[albumIdx].albumTagListSel) >= 0 ) {
2974              tagClass='Selected';
2975              ic=G.O.icons.navigationFilterSelected;
2976            }
2977            var $newTag=jQuery('<div class="nGY2NavigationbarItem nGY2NavFilter'+tagClass+'">'+ic+' '+s+'</div>').appendTo(G.GOM.navigationBar.$newContent);
2978            $newTag.click(function() {
2979              var $this=jQuery(this);
2980              var tag=$this.text().replace(/^\s*|\s*$/, '');  //trim trailing/leading whitespace
2981              // if( $this.hasClass('oneTagUnselected') ){
2982              if( $this.hasClass('nGY2NavFilterUnselected') ){
2983                G.I[albumIdx].albumTagListSel.push(tag);
2984              }
2985              else {
2986                var tidx=jQuery.inArray(tag,G.I[albumIdx].albumTagListSel);
2987                if( tidx != -1 ) {
2988                  G.I[albumIdx].albumTagListSel.splice(tidx,1);
2989                }
2990              }
2991              $this.toggleClass('nGY2NavFilters-oneTagUnselected nGY2NavFilters-oneTagSelected');
2992              DisplayAlbum('-1', G.I[albumIdx].GetID());
2993            });
2994          }
2995          var $newClearFilter=jQuery('<div class="nGY2NavigationbarItem nGY2NavFilterSelectAll">'+G.O.icons.navigationFilterSelectedAll+'</div>').appendTo(G.GOM.navigationBar.$newContent);
2996          $newClearFilter.click(function() {
2997            var nTags=G.I[albumIdx].albumTagList.length;
2998            G.I[albumIdx].albumTagListSel=[];
2999            for(var i=0; i <nTags; i++ ) {
3000              var s=G.I[albumIdx].albumTagList[i];
3001              G.I[albumIdx].albumTagListSel.push(s);
3002            }
3003            DisplayAlbum('-1', G.I[albumIdx].GetID());
3004          });
3005        }
3006      }
3007
3008    }
3009    
3010    function BreadcrumbBuild(lstItems) {
3011
3012    // console.log(G.GOM.navigationBar.$newContent);
3013    jQuery('<div class="nGY2NavigationbarItem nGY2Breadcrumb"></div>').appendTo(G.GOM.navigationBar.$newContent);
3014    // console.log(G.GOM.navigationBar.$newContent);
3015      
3016      if( G.O.breadcrumbOnlyCurrentLevel ) {
3017        // display only 1 separator and the current folder level
3018        if( lstItems.length == 0 ) {
3019          breadcrumbAdd(0);
3020        }
3021        else {
3022          var last=lstItems.length-1;
3023          if( lstItems.length == 1 ) {
3024            breadcrumbAddSeparator(0);    // root level
3025          }
3026          else {
3027            breadcrumbAddSeparator(lstItems[0]);
3028          }
3029          breadcrumbAdd(lstItems[0]);
3030        }
3031      }
3032      else {
3033        // display the full breadcrum (full folder levels including root level)
3034        breadcrumbAdd(0);
3035        if( lstItems.length > 0 ) {
3036          breadcrumbAddSeparator(0);
3037          for(var i=lstItems.length-1; i>=0 ; i-- ) {
3038            breadcrumbAdd(lstItems[i]);
3039            if( i > 0 ) {
3040              breadcrumbAddSeparator(lstItems[i-1]);
3041            }
3042          }
3043        }
3044      }
3045    
3046    }
3047    
3048
3049    // Display gallery pagination
3050    function ManagePagination( albumIdx ) {
3051
3052      G.$E.conTnBottom.css('opacity', 0);
3053      G.$E.conTnBottom.children().remove();
3054
3055      if( G.GOM.items.length == 0 ) { return; }   // no thumbnail to display
3056
3057      // calculate the number of pages
3058      var nbPages=Math.ceil((G.GOM.items[G.GOM.items.length - 1].row + 1)/G.galleryMaxRows.Get());
3059
3060      // only one page -> do not display pagination
3061      if( nbPages == 1 ) { return; }
3062
3063      // check if current page still exists (for example after a resize)
3064      if( G.GOM.pagination.currentPage > (nbPages-1) ) {
3065        G.GOM.pagination.currentPage = nbPages-1;
3066      }
3067      
3068      GalleryRenderGetInterval();
3069      // nothing to display --> exit
3070      if( G.GOM.displayInterval.len == 0 ) { return; }
3071      
3072      // display "previous"
3073      if( G.O.galleryPaginationMode == 'NUMBERS' && G.GOM.pagination.currentPage > 0 ) {
3074        var $eltPrev = jQuery('<div class="nGY2PaginationPrev">'+G.O.icons.paginationPrevious+'</div>').appendTo(G.$E.conTnBottom);
3075        $eltPrev.click(function(e) {
3076          paginationPreviousPage();
3077        });
3078      }
3079
3080      var firstPage = 0;
3081      var lastPage = nbPages;
3082      if( G.O.galleryPaginationMode != 'NUMBERS' ) {
3083        // no 'previous'/'next' and no max number of pagination items
3084        firstPage = 0;
3085      }
3086      else {
3087        // display pagination numbers and previous/next
3088        var vp = G.O.paginationVisiblePages;
3089        var numberOfPagesToDisplay = G.O.paginationVisiblePages;
3090        if( numberOfPagesToDisplay >= nbPages ) {
3091          firstPage = 0;
3092        }
3093        else {
3094          // we have more pages than we want to display
3095          var nbBeforeAfter = 0;
3096          if( isOdd(numberOfPagesToDisplay) ) {
3097            nbBeforeAfter = (numberOfPagesToDisplay + 1) / 2;
3098          }
3099          else {
3100            nbBeforeAfter = numberOfPagesToDisplay / 2;
3101          }
3102          
3103          if( G.GOM.pagination.currentPage < nbBeforeAfter ) {
3104            firstPage = 0;
3105            lastPage = numberOfPagesToDisplay - 1;
3106            if( lastPage > nbPages ) {
3107              lastPage = nbPages - 1;
3108            }
3109          }
3110          else {
3111            firstPage = G.GOM.pagination.currentPage - nbBeforeAfter;
3112            lastPage = firstPage + numberOfPagesToDisplay;
3113            if( lastPage > nbPages ) {
3114              lastPage = nbPages - 1;
3115            }
3116          }
3117          
3118          if( (lastPage - firstPage) < numberOfPagesToDisplay ) {
3119            firstPage = lastPage - numberOfPagesToDisplay;
3120            if( firstPage < 0 ) {
3121              firstPage = 0;
3122            }
3123          }
3124
3125        }
3126      }
3127
3128      // render pagination items
3129      for(var i = firstPage; i < lastPage; i++ ) {
3130        var c = '';
3131        var p = '';
3132
3133        switch( G.O.galleryPaginationMode ) {
3134          case 'NUMBERS':
3135            c = 'nGY2paginationItem';
3136            p = i + 1;
3137            break;
3138          case 'DOTS':
3139            c = 'nGY2paginationDot';
3140            break;
3141          case 'RECTANGLES':
3142            c = 'nGY2paginationRectangle';
3143            break;
3144        }
3145        if( i == G.GOM.pagination.currentPage ) {
3146          c += 'CurrentPage';
3147        }
3148
3149        var elt$ = jQuery('<div class="' + c + '">' + p + '</div>').appendTo(G.$E.conTnBottom);
3150        elt$.data('pageNumber', i );
3151        elt$.click( function(e) {
3152          G.GOM.pagination.currentPage = jQuery(this).data('pageNumber');
3153          TriggerCustomEvent('pageChanged');
3154
3155          // scroll to top of gallery if not displayed
3156          if( !inViewportVert(G.$E.base, 0) ) {
3157            $('html, body').animate({scrollTop: G.$E.base.offset().top}, 200);
3158          }
3159          GalleryDisplayPart1( true );
3160          GalleryDisplayPart2( true );
3161        });
3162
3163      }
3164
3165      // display "next"
3166      if( G.O.galleryPaginationMode == 'NUMBERS' && (G.GOM.pagination.currentPage + 1) < nbPages ) {
3167        var $eltNext = jQuery('<div class="nGY2PaginationNext">' + G.O.icons.paginationNext + '</div>').appendTo(G.$E.conTnBottom);
3168        $eltNext.click( function(e) {
3169          paginationNextPage();
3170        });
3171      }
3172
3173      G.$E.conTnBottom.css('opacity', 1);
3174
3175    }
3176    function isOdd(num) { return (num % 2) == 1;}
3177    
3178    // pagination - next page
3179    function paginationNextPage() {
3180      var aIdx = G.GOM.albumIdx,
3181      n1 = 0;
3182      ThumbnailHoverOutAll();
3183      
3184      // pagination - max lines per page mode
3185      if( G.galleryMaxRows.Get() > 0 ) {
3186        // number of pages
3187        n1 = (G.GOM.items[G.GOM.items.length - 1].row + 1) / G.galleryMaxRows.Get();
3188      }
3189      var n2 = Math.ceil(n1);
3190      var pn = G.GOM.pagination.currentPage;
3191      if( pn < (n2-1) ) {
3192        pn++;
3193      }
3194      else {
3195        pn = 0;
3196      }
3197      
3198      G.GOM.pagination.currentPage = pn;
3199      TriggerCustomEvent('pageChanged');
3200
3201      // scroll to top of gallery if not displayed
3202      if( !inViewportVert(G.$E.base, 0) ) {
3203        $('html, body').animate({scrollTop: G.$E.base.offset().top}, 200);
3204      }
3205      GalleryDisplayPart1( true );
3206      GalleryDisplayPart2( true );
3207    }
3208    
3209    // pagination - previous page
3210    function paginationPreviousPage() {
3211      // var aIdx=G.$E.conTnBottom.data('galleryIdx'),
3212      var aIdx = G.GOM.albumIdx,
3213      n1 = 0;
3214
3215      ThumbnailHoverOutAll();
3216      
3217      // pagination - max lines per page mode
3218      if( G.galleryMaxRows.Get() > 0 ) {
3219        // number of pages
3220        n1 = (G.GOM.items[G.GOM.items.length - 1].row + 1) / G.galleryMaxRows.Get();
3221      }
3222      var n2 = Math.ceil(n1);
3223      
3224      // var pn=G.$E.conTnBottom.data('currentPageNumber');
3225      var pn = G.GOM.pagination.currentPage;
3226      if( pn > 0 ) {
3227        pn--;
3228      }
3229      else {
3230        pn = n2 - 1;
3231      }
3232
3233      G.GOM.pagination.currentPage = pn;
3234      TriggerCustomEvent('pageChanged');
3235
3236      // scroll to top of gallery if not displayed
3237      if( !inViewportVert(G.$E.base, 0) ) {
3238        $('html, body').animate({scrollTop: G.$E.base.offset().top}, 200);
3239      }
3240      GalleryDisplayPart1( true );
3241      GalleryDisplayPart2( true );
3242    }
3243
3244    // retrieve the from/to intervall for gallery thumbnail render
3245    function GalleryRenderGetInterval() {
3246      G.GOM.displayInterval.from = 0;
3247      G.GOM.displayInterval.len = G.I.length;
3248      
3249      switch( G.galleryDisplayMode.Get() ) {
3250        case 'PAGINATION':
3251          if( G.layout.support.rows ) {
3252            var nbTn = G.GOM.items.length;
3253            var firstRow = G.GOM.pagination.currentPage * G.galleryMaxRows.Get();
3254            var lastRow = firstRow + G.galleryMaxRows.Get();
3255            var firstTn = -1;
3256            G.GOM.displayInterval.len = 0;
3257            for( var i = 0; i < nbTn ; i++ ) {
3258              var curTn = G.GOM.items[i];
3259              if( curTn.row >= firstRow && curTn.row < lastRow ) {
3260                if( firstTn == -1 ) {
3261                  G.GOM.displayInterval.from = i;
3262                  firstTn = i;
3263                }
3264                G.GOM.displayInterval.len++;
3265              }
3266            }
3267          }
3268          break;
3269        case 'MOREBUTTON':
3270          if( G.layout.support.rows ) {
3271            var nbTn = G.GOM.items.length;
3272            var lastRow = G.O.galleryDisplayMoreStep * (G.GOM.displayedMoreSteps+1);
3273            G.GOM.displayInterval.len = 0;
3274            for( var i = 0; i < nbTn ; i++ ) {
3275              var curTn = G.GOM.items[i];
3276              if( curTn.row < lastRow ) {
3277                G.GOM.displayInterval.len++;
3278              }
3279            }
3280          }
3281          break;
3282        case 'ROWS':
3283          if( G.layout.support.rows ) {
3284            var nbTn = G.GOM.items.length;
3285            var lastRow = G.galleryMaxRows.Get();
3286            if( G.galleryLastRowFull.Get() && G.GOM.lastFullRow != -1 ) {
3287              if( lastRow > (G.GOM.lastFullRow + 1) ) {
3288                lastRow = G.GOM.lastFullRow + 1;
3289              }
3290            }
3291            G.GOM.displayInterval.len = 0;
3292            for( var i = 0; i < nbTn ; i++ ) {
3293              var curTn = G.GOM.items[i];
3294              if( curTn.row < lastRow ) {
3295                G.GOM.displayInterval.len++;
3296              }
3297            }
3298          }
3299          break;
3300        default:
3301        case 'FULLCONTENT':
3302        if( G.layout.support.rows && G.galleryLastRowFull.Get() && G.GOM.lastFullRow != -1 ) {
3303            var nbTn = G.GOM.items.length;
3304            var lastRow = G.GOM.lastFullRow + 1;
3305            G.GOM.displayInterval.len = 0;
3306            for( var i = 0; i < nbTn ; i++ ) {
3307              var curTn = G.GOM.items[i];
3308              if( curTn.row < lastRow ) {
3309                G.GOM.displayInterval.len++;
3310              }
3311            }
3312          }
3313          break;
3314      }
3315    }
3316    
3317    
3318    // RENDER THE GALLERY
3319    function GalleryRender( albumIdx ) {
3320      TriggerCustomEvent('galleryRenderStart');
3321      
3322      clearTimeout(G.GOM.slider.timerID);
3323      G.GOM.slider.hostIdx = -1;      // disabled slider on thumbnail
3324      
3325      var fu=G.O.fnGalleryRenderStart;
3326      if( fu !== null ) {
3327        typeof fu == 'function' ? fu(albumIdx) : window[fu](albumIdx);
3328      }
3329
3330      G.layout.SetEngine();
3331      G.galleryResizeEventEnabled = false;
3332      G.GOM.albumIdx = -1;
3333      G.GOM.lastDisplayedIdx = -1;
3334
3335      // pagination
3336      if( G.$E.conTnBottom !== undefined ) {
3337        // G.$E.conTnBottom.children().remove();
3338        G.$E.conTnBottom.empty();
3339      }
3340
3341      // navigation toolbar (breadcrumb + tag filters)
3342      GalleryNavigationBar(albumIdx);
3343      
3344      if( G.GOM.firstDisplay ) {
3345        G.GOM.firstDisplay = false;
3346        var d = Date.now()-G.GOM.firstDisplayTime;
3347        if( d < G.O.galleryRenderDelay ) {
3348          // setTimeout( function() { GalleryRenderPart1( albumIdx )}, G.O.galleryRenderDelay-d );
3349          requestTimeout( function() { GalleryRenderPart1( albumIdx )}, G.O.galleryRenderDelay-d );
3350        }
3351        else {
3352          GalleryRenderPart1( albumIdx );
3353        }
3354        G.O.galleryRenderDelay = 0;
3355        
3356      }
3357      else {
3358        var hideNavigationBar = false;
3359        if( G.GOM.navigationBar.$newContent.children().length == 0 ) {
3360          hideNavigationBar = true;
3361        }
3362
3363        // hide everything
3364        var tweenable = new NGTweenable();
3365        tweenable.tween({
3366          from:         { 'opacity': 1 },
3367          to:           { 'opacity': 0 },
3368          duration:     300,
3369          easing:       'easeInQuart',
3370          attachment:   { h: hideNavigationBar },
3371          step:         function (state, att) {
3372            // window.ng_draf( function() {
3373              G.$E.conTnParent.css({'opacity': state.opacity });
3374              if( att.h ) {
3375                G.$E.conNavigationBar.css({ 'opacity': state.opacity });
3376              }
3377            // });
3378          },
3379          finish:       function (state, att) {
3380            // window.ng_draf( function() {
3381              if( att.h ) {
3382                G.$E.conNavigationBar.css({ 'opacity': 0, 'display': 'none' });
3383              }
3384              // scroll to top of the gallery if needed
3385              var galleryOTop = G.$E.base.offset().top;
3386              if( galleryOTop < G.GOM.cache.viewport.t ) {
3387                // jQuery('html, body').animate({scrollTop: galleryOTop}, 200);
3388                jQuery('html, body').animate({scrollTop: galleryOTop}, 500, "linear", function() {
3389                  GalleryRenderPart1( albumIdx );
3390                });
3391              }
3392              else {
3393                GalleryRenderPart1( albumIdx );
3394              }
3395            // });
3396          }
3397        });
3398      }
3399    }
3400
3401
3402    function GalleryRenderPart1( albumIdx ) {
3403      // display new navigation bar
3404      var oldN = G.$E.conNavigationBar.children().length;
3405      G.$E.conNavigationBar.empty();
3406      G.GOM.navigationBar.$newContent.children().clone(true,true).appendTo(G.$E.conNavigationBar);
3407      // G.GOM.navigationBar.$newContent.appendTo(G.$E.conNavigationBar);
3408      if( G.$E.conNavigationBar.children().length > 0 && oldN == 0 ) {
3409        G.$E.conNavigationBar.css({ 'opacity': 0, 'display': 'block' });
3410        var tweenable = new NGTweenable();
3411        tweenable.tween({
3412          from:     { opacity: 0 },
3413          to:       { opacity: 1 },
3414          duration: 200,
3415          easing:   'easeInQuart',
3416          step:     function (state) {
3417            // window.ng_draf( function() {
3418              G.$E.conNavigationBar.css( state );
3419            // });
3420          },
3421          finish:   function (state) {
3422            // window.ng_draf( function() {
3423              G.$E.conNavigationBar.css({ 'opacity': 1 });
3424              // display gallery
3425              // GalleryRenderPart2( albumIdx );
3426              // setTimeout(function(){ GalleryRenderPart2(albumIdx) }, 60);
3427              requestTimeout(function(){ GalleryRenderPart2(albumIdx) }, 60);
3428            // });
3429          }
3430        });
3431      }
3432      else {
3433        // display gallery
3434        // GalleryRenderPart2( albumIdx );
3435            // setTimeout(function(){ GalleryRenderPart2(albumIdx) }, 60);
3436            requestTimeout(function(){ GalleryRenderPart2(albumIdx) }, 60);
3437      }
3438
3439    }
3440    
3441    // Gallery render part 2 -> remove all thumbnails
3442    function GalleryRenderPart2(albumIdx) {
3443      G.GOM.lastZIndex = parseInt(G.$E.base.css('z-index'));
3444      if( isNaN(G.GOM.lastZIndex) ) {
3445        G.GOM.lastZIndex=0;
3446      }
3447      G.$E.conTnParent.css({ 'opacity': 0 });
3448      G.$E.conTn.off().empty();
3449      var l = G.I.length;
3450      for( var i = 0; i < l ; i++ ) {
3451        // reset each item
3452        var item = G.I[i];
3453        item.hovered = false;
3454        item.$elt = null;
3455        item.$Elts = [];
3456        item.eltTransform = [];
3457        item.eltFilter = [];
3458        item.width = 0;
3459        item.height = 0;
3460        item.left = 0;
3461        item.top = 0;
3462        item.resizedContentWidth = 0;
3463        item.resizedContentHeight = 0;
3464        item.thumbnailImgRevealed = false;
3465      }
3466
3467      if( G.CSStransformName == null ) {
3468        G.$E.conTn.css('left', '0px');
3469      }
3470      else {
3471        // G.$E.conTn.css( G.CSStransformName, 'translateX(0px)');
3472        G.$E.conTn.css( G.CSStransformName, 'none');
3473      }
3474      
3475      // setTimeout(function(){ GalleryRenderPart3(albumIdx) }, 60);
3476      requestTimeout(function(){ GalleryRenderPart3(albumIdx) }, 60);
3477      // GalleryRenderPart3(albumIdx);
3478
3479    }
3480    
3481    // Gallery render part 2 -> start building the new gallery
3482    function GalleryRenderPart3(albumIdx) {
3483      var d = new Date();      
3484      
3485      G.$E.conTnParent.css( 'opacity', 1);
3486
3487      G.GOM.items = [];
3488      G.GOM.displayedMoreSteps = 0;
3489      // retrieve label height      
3490      if( G.O.thumbnailLabel.get('position') == 'onBottom' ) {
3491        // retrieve height each time because size can change depending on thumbnail's settings
3492        G.tn.labelHeight[G.GOM.curNavLevel]=ThumbnailGetLabelHeight();
3493      }
3494      else {
3495        G.tn.labelHeight[G.GOM.curNavLevel]=0;
3496      }
3497      G.GOM.albumIdx=albumIdx;
3498
3499      TriggerCustomEvent('galleryRenderEnd');
3500      var fu=G.O.fnGalleryRenderEnd;
3501      if( fu !== null ) {
3502        typeof fu == 'function' ? fu(albumIdx) : window[fu](albumIdx);
3503      }
3504
3505      // Step 1: populate GOM
3506      if( GalleryPopulateGOM() ) {
3507
3508        // step 2: calculate layout
3509        GallerySetLayout();
3510
3511        // step 3: display gallery
3512        GalleryAppear();
3513        // GalleryDisplay( false );
3514        GalleryDisplayPart1( false );
3515        // setTimeout(function(){ GalleryDisplayPart2( false ) }, 120);
3516        requestTimeout(function(){ GalleryDisplayPart2( false ) }, 120);
3517      }
3518      else {
3519        G.galleryResizeEventEnabled = true;
3520      }
3521      
3522      if( G.O.debugMode ) { console.log('GalleryRenderPart3: '+ (new Date()-d)); }
3523
3524    }
3525    
3526    
3527    // Resize the gallery
3528    function GalleryResize() {
3529      var d = new Date();
3530      G.galleryResizeEventEnabled = false;
3531      // G.GOM.cache.areaWidth=G.$E.conTnParent.width();
3532      if( GallerySetLayout() == false ) {
3533        G.galleryResizeEventEnabled = true;
3534        if( G.O.debugMode ) { console.log('GalleryResize1: '+ (new Date()-d)); }
3535        return;
3536      }
3537      if( G.O.debugMode ) { console.log('GalleryResizeSetLayout: '+ (new Date()-d)); }
3538
3539      GalleryDisplayPart1( false );
3540      GalleryDisplayPart2( false );
3541
3542      if( G.O.debugMode ) { console.log('GalleryResizeFull: '+ (new Date()-d)); }
3543    }
3544    
3545    
3546    
3547    // copy items (album content) to GOM
3548    function GalleryPopulateGOM() {
3549      
3550      var preloadImages = '';
3551      var imageSizeRequested = false;
3552      var albumID = G.I[G.GOM.albumIdx].GetID();
3553      var l = G.I.length;
3554      var cnt = 0;
3555
3556      for( var idx = 0; idx < l; idx++ ) {
3557        var item = G.I[idx];
3558        // check album
3559        if( item.isToDisplay(albumID) ) {
3560          var w = item.thumbImg().width;
3561          var h = item.thumbImg().height;
3562          // if unknown image size and layout is not grid --> we need to retrieve the size of the images
3563          if( G.layout.prerequisite.imageSize && ( w == 0 || h == 0) ) {
3564          // if( true ) {
3565            imageSizeRequested = true;
3566            preloadImages += '<img src="'+item.thumbImg().src+'" data-idx="'+cnt+'" data-albumidx="'+G.GOM.albumIdx+'">';
3567          }
3568          
3569          // set default size if required
3570          if( h == 0 ) {
3571            h = G.tn.defaultSize.getHeight();
3572          }
3573          if( w == 0 ) {
3574            w = G.tn.defaultSize.getWidth();
3575          }
3576          var tn = new G.GOM.GTn(idx, w, h);
3577          G.GOM.items.push(tn);
3578          cnt++;
3579        }
3580      }
3581
3582      TriggerCustomEvent('galleryObjectModelBuilt');
3583      var fu = G.O.fnGalleryObjectModelBuilt;
3584      if( fu !== null ) {
3585        typeof fu == 'function' ? fu() : window[fu]();
3586      }
3587      
3588      if( imageSizeRequested ) {
3589        // preload images to retrieve their size and then resize the gallery (=GallerySetLayout()+ GalleryDisplay())
3590        var $newImg = jQuery(preloadImages);
3591        var gi_imgLoad = ngimagesLoaded( $newImg );
3592        $newImg = null;
3593        gi_imgLoad.on( 'progress', function( instance, image ) {
3594        
3595          if( image.isLoaded ) {
3596            var idx = image.img.getAttribute('data-idx');
3597            var albumIdx = image.img.getAttribute('data-albumidx');
3598            if( albumIdx == G.GOM.albumIdx ) {
3599              // ignore event if not on current album
3600              var curTn = G.GOM.items[idx];
3601              curTn.imageWidth = image.img.naturalWidth;
3602              curTn.imageHeight = image.img.naturalHeight;
3603              var item = G.I[curTn.thumbnailIdx];
3604              item.thumbs.width[G.GOM.curNavLevel][G.GOM.curWidth] = curTn.imageWidth;
3605              item.thumbs.height[G.GOM.curNavLevel][G.GOM.curWidth] = curTn.imageHeight;
3606 
3607              // resize the gallery
3608              G.GalleryResizeThrottled();
3609              
3610              // set the retrieved size to all levels with same configuration  
3611              var object = item.thumbs.width.l1;
3612              for (var property in object) {
3613                if (object.hasOwnProperty(property)) {
3614                  if( property != G.GOM.curWidth ) {
3615                    if( G.tn.settings.width.l1[property] == G.tn.settings.getW() && G.tn.settings.height.l1[property] == G.tn.settings.getH() ) {
3616                      item.thumbs.width.l1[property] = curTn.imageWidth;
3617                      item.thumbs.height.l1[property] = curTn.imageHeight;
3618                    }
3619                  }
3620                }
3621              }
3622              object = item.thumbs.width.lN;
3623              for (var property in object) {
3624                if (object.hasOwnProperty(property)) {
3625                  if( property != G.GOM.curWidth ) {
3626                    if( G.tn.settings.width.lN[property] == G.tn.settings.getW() && G.tn.settings.height.lN[property] == G.tn.settings.getH() ) {
3627                      item.thumbs.width.lN[property] = curTn.imageWidth;
3628                      item.thumbs.height.lN[property] = curTn.imageHeight;
3629                    }
3630                  }
3631                }
3632              }
3633            }
3634          }
3635        });
3636        G.galleryResizeEventEnabled = true;
3637        return false;
3638      }
3639      else {
3640        return true;
3641      }
3642      
3643    }
3644    
3645    //----- Calculate the layout of the thumbnails
3646    function GallerySetLayout() {
3647      var r = true;
3648      // width of the available area
3649      G.GOM.cache.areaWidth = G.$E.conTnParent.width();
3650      G.GOM.displayArea = { width:0, height:0 };
3651
3652      switch( G.layout.engine ) {
3653        case 'JUSTIFIED':
3654          r = GallerySetLayoutWidthtAuto();
3655          break;
3656        case 'CASCADING':
3657          r = GallerySetLayoutHeightAuto();
3658          break;
3659        case 'MOSAIC':
3660          r = GallerySetLayoutMosaic();
3661          break;
3662        case 'GRID':
3663        default:
3664          r = GallerySetLayoutGrid();
3665          break;
3666      }
3667      
3668      TriggerCustomEvent('galleryLayoutApplied');
3669      var fu = G.O.fnGalleryLayoutApplied;
3670      if( fu !== null ) {
3671        typeof fu == 'function' ? fu() : window[fu]();
3672      }
3673      return r;
3674
3675    }
3676    
3677    
3678    //----- CASCADING LAYOUT
3679    function GallerySetLayoutHeightAuto() {
3680      var curCol =      0,
3681      areaWidth =       G.GOM.cache.areaWidth,
3682      curRow =          0,
3683      colHeight =       [],
3684      maxCol =          NbThumbnailsPerRow(areaWidth),
3685      gutterWidth =     0,
3686      gutterHeight =    G.tn.opt.Get('gutterHeight');
3687      var w =           0;
3688      var scaleFactor = 1;
3689      var tnWidth =     G.tn.defaultSize.getOuterWidth();
3690      var nbTn =        G.GOM.items.length;
3691      var curPosY = 0;
3692
3693      if( G.O.thumbnailAlignment == 'justified' ) {
3694        maxCol = Math.min(maxCol, nbTn);
3695        gutterWidth = ( maxCol == 1 ? 0 : (areaWidth - (maxCol * tnWidth) ) / (maxCol - 1) );
3696      }
3697      else {
3698        gutterWidth=G.tn.opt.Get('gutterWidth');
3699      }
3700
3701
3702      var borderWidth = G.tn.borderWidth * 2;
3703      var borderHeight = G.tn.borderHeight * 2;
3704
3705      G.GOM.lastFullRow=-1;   // feature disabled
3706
3707      // Retrieve the real used width of the area (the evaluation is based on the content of the first line)
3708      if( G.O.thumbnailAlignment == 'fillWidth' ) {
3709        // fillWidth --> evaluate scale factor and number of columns
3710        var totalGutterWidth = (maxCol - 1) * gutterWidth;
3711        scaleFactor = (areaWidth - totalGutterWidth) / (maxCol * tnWidth);
3712        if( scaleFactor > 1 ) {
3713          maxCol++; // add one column and re-evaluate the scale factor
3714        }
3715        totalGutterWidth = (maxCol - 1) * gutterWidth;
3716        scaleFactor = Math.min( (areaWidth - totalGutterWidth) / (maxCol*tnWidth), 1);   // no upscale
3717      }
3718
3719      
3720      tnWidth = tnWidth * scaleFactor;
3721      var contentWidth = tnWidth - borderWidth;
3722
3723      // loop to position the thumbnails, and set their size
3724      var baseHeight = G.tn.opt.Get('baseGridHeight') * scaleFactor;
3725      for( var i = 0; i < nbTn ; i++ ) {
3726        var curTn = G.GOM.items[i];
3727        if( curTn.deleted == true ) { break; }    // item is logically deleted
3728        if( curTn.imageHeight > 0 && curTn.imageWidth > 0 ) {
3729          var curPosX = 0,
3730          curPosY = 0;
3731          var imageRatio = curTn.imageHeight / curTn.imageWidth;
3732          // curTn.resizedContentWidth = tnWidth - borderWidth;
3733          curTn.resizedContentWidth = contentWidth;
3734          curTn.resizedContentHeight = curTn.resizedContentWidth * imageRatio;
3735          if( baseHeight > 0 ) {
3736            // grid based vertical position
3737            var t= Math.max( Math.trunc(curTn.resizedContentHeight/baseHeight), 1) ;
3738            curTn.resizedContentHeight = baseHeight * t + ((t-1)*(borderHeight+gutterHeight));
3739          }
3740          
3741          curTn.height = curTn.resizedContentHeight + borderHeight + G.tn.labelHeight.get();
3742          curTn.width = tnWidth;
3743          curTn.row = 0;
3744          
3745          if( curRow == 0 ) {
3746            // first row
3747            curPosX = curCol * (tnWidth + gutterWidth);
3748            colHeight[curCol] = curTn.height + gutterHeight;
3749            
3750            curCol++;
3751            if( curCol >= maxCol ) {
3752              curCol = 0;
3753              curRow++;
3754            }
3755          }
3756          else {
3757            var c=0,
3758            minColHeight=colHeight[0];
3759            for( var j = 1; j < maxCol; j++) {
3760              if( (colHeight[j] + 5) < minColHeight ) {     // +5 --> threshold
3761                minColHeight = colHeight[j];
3762                c = j;
3763                //break;
3764              }
3765            }
3766            curPosY = colHeight[c];
3767            curPosX = c * (tnWidth + gutterWidth);
3768            colHeight[c] = curPosY + curTn.height + gutterHeight;
3769          }
3770
3771          var x = curPosX;
3772          if( G.O.RTL) {
3773            x= w - curPosX - tnWidth;
3774          }
3775
3776          curTn.left = x;
3777          curTn.top = curPosY;
3778        }
3779      }
3780
3781      G.GOM.displayArea.width= maxCol * (tnWidth + gutterWidth) - gutterWidth;
3782      return true;
3783    }
3784    
3785    
3786    //----- JUSTIFIED LAYOUT
3787    function GallerySetLayoutWidthtAuto() {
3788      var curWidth =               0,
3789      areaWidth =                  G.GOM.cache.areaWidth,
3790      lastPosX =                   0,
3791      curPosY =                    0,
3792      rowLastItem =                [],
3793      rowNum =                     0,
3794      rowHeight =                  [],
3795      bNewRow =                    false,
3796      cnt =                        0,
3797      gutterWidth =                G.tn.opt.Get('gutterWidth'),
3798      gutterHeight =               G.tn.opt.Get('gutterHeight');
3799      // by grief-of-these-days
3800      var maxRowHeightVertical =   0; // max height of a row with vertical thumbs
3801      var maxRowHeightHorizontal = 0; // max height of a row with horizontal thumbs
3802      var rowHasVertical =         false; // current row has vertical thumbs
3803      var rowHasHorizontal =       false; // current row has horizontal thumbs
3804
3805      var tnHeight = G.tn.defaultSize.getOuterHeight();
3806      var borderWidth = G.tn.borderWidth * 2;
3807      var borderHeight = G.tn.borderHeight * 2;
3808      var nbTnInCurrRow = 1;
3809      var nbTn = G.GOM.items.length;
3810
3811      // first loop --> retrieve each row image height
3812      for( var i = 0; i < nbTn ; i++ ) {
3813        var curTn = G.GOM.items[i];
3814        if( curTn.deleted == true ) { break; }    // item is logically deleted
3815        if( curTn.imageWidth > 0 ) {
3816          var imageRatio = curTn.imageWidth / curTn.imageHeight;
3817          var imageWidth = Math.floor( tnHeight * imageRatio );
3818
3819          if( bNewRow ) {
3820            bNewRow = false;
3821            rowNum++;
3822            curWidth = 0;
3823            rowHasVertical = false;
3824            rowHasHorizontal = false;
3825            nbTnInCurrRow = 1;
3826          }
3827          // by grief-of-these-days
3828          if( curTn.imageHeight > curTn.imageWidth ) {
3829            rowHasVertical = true;
3830          }
3831          else {
3832            rowHasHorizontal = true;
3833          }
3834          
3835          if( (curWidth + gutterWidth + imageWidth) < (areaWidth - (nbTnInCurrRow * borderWidth)) ) {
3836            // enough place left in the current row
3837            curWidth += imageWidth + gutterWidth;
3838            rowHeight[rowNum] = tnHeight;
3839            
3840            // prevent incomplete row from being heigher than the previous ones.
3841            // by grief-of-these-days
3842            var rowHeightLimit = Math.max(rowHasVertical ? maxRowHeightVertical : 0, rowHasHorizontal ? maxRowHeightHorizontal : 0);
3843            if( rowHeightLimit > 0 ) {
3844              rowHeight[rowNum] = Math.min(rowHeight[rowNum], rowHeightLimit);
3845            }
3846            
3847            rowLastItem[rowNum] = i;
3848          }
3849          else {
3850            // new row after current item --> we need to adujet the row height to have enough space for the current thumbnail
3851            curWidth += gutterWidth+imageWidth;
3852            var ratio = (areaWidth - nbTnInCurrRow * borderWidth) / curWidth;
3853            var rH = Math.floor(tnHeight * ratio);
3854            rowHeight[rowNum] = rH;
3855            
3856            // save the max row height for each thumb orientation.
3857            // by grief-of-these-days
3858            if( rowHasVertical ) {
3859              maxRowHeightVertical = Math.max( maxRowHeightVertical, rH );
3860            }
3861            if( rowHasHorizontal ) {
3862              maxRowHeightHorizontal = Math.max( maxRowHeightHorizontal, rH );
3863            }
3864            
3865            rowLastItem[rowNum] = i;
3866            bNewRow = true;
3867          }
3868          cnt++;
3869          nbTnInCurrRow++;
3870        }
3871      }
3872
3873      rowNum = 0;
3874      curPosY = 0;
3875      lastPosX = 0;
3876      cnt = 0;
3877      
3878      G.GOM.lastFullRow = 0;    // display at leat 1 row (even if not full)
3879      
3880      // second loop --> calculate each thumbnail size
3881      for( var i = 0; i < nbTn ; i++ ) {
3882        var curTn = G.GOM.items[i];
3883        if( curTn.imageWidth > 0 ) {
3884          var imageRatio = curTn.imageWidth / curTn.imageHeight;
3885          var imageWidth = Math.floor( imageRatio * rowHeight[rowNum] ); // border is already NOT included
3886
3887          if( i == rowLastItem[rowNum] ) {
3888            // row last item --> adjust image width because of rounding problems
3889            if( rowLastItem.length != (rowNum+1) ) {
3890              // last item in current row -> use the full remaining width
3891              imageWidth = areaWidth - lastPosX - borderWidth;
3892            }
3893            else {
3894              // very last item (on the last row)
3895              if( (lastPosX + gutterWidth + imageWidth + borderWidth ) > areaWidth ) {
3896                // reduce size if image is wider as the remaining space
3897                imageWidth = areaWidth - lastPosX - borderWidth;
3898              }
3899            }
3900          }
3901          
3902          var rh = parseInt( rowHeight[rowNum] );
3903          imageWidth = parseInt( imageWidth );
3904
3905          // thumbnail image size
3906          curTn.resizedContentWidth = imageWidth;
3907          curTn.resizedContentHeight = rh;
3908          // thumbnail position and size
3909          curTn.width = imageWidth + borderWidth;
3910          curTn.height= rh + G.tn.labelHeight.get() + borderHeight;
3911          curTn.row = rowNum;
3912
3913          curTn.top = curPosY;
3914          var x = lastPosX;
3915          if( G.O.RTL) {
3916            x = areaWidth - lastPosX - curTn.width ;
3917          }
3918          curTn.left = x;
3919
3920          lastPosX += curTn.width + gutterWidth;
3921
3922          if( i == rowLastItem[rowNum] ) {
3923            // start a new row
3924            curPosY += curTn.height + gutterHeight;
3925            G.GOM.lastFullRow = rowNum - 1;
3926            rowNum++;
3927            lastPosX = 0;
3928          }
3929          cnt++;
3930        }
3931        else {
3932          return false;
3933        }
3934      }
3935      
3936      if( false ) {
3937        var newTop = 0;
3938        if( typeof GOMidx !== 'undefined' ) {
3939          // hover effect on gallery (vs on thumbnail) --> experimental / not used
3940          if( G.GOM.albumIdx != -1 ) {
3941            var hoveredTn = G.GOM.items[GOMidx];
3942            var item = G.I[hoveredTn.thumbnailIdx];
3943            
3944            // hovered thumbnail
3945            hoveredTn.width += 40;
3946            hoveredTn.height += 40;
3947            // todo : left
3948            
3949            for( var i = 0; i < nbTn ; i++ ) {
3950              var curTn = G.GOM.items[i];
3951              if( curTn.imageWidth > 0 ) {
3952                if( curTn.row == hoveredTn.row ) {
3953                  // hovered row
3954                  newTop = 40;
3955                  if( hoveredTn.thumbnailIdx != curTn.thumbnailIdx ) {
3956                    // not hovered thumbnail
3957                    // curTn.resizedContentWidth+=10;
3958                    // curTn.resizedContentHeight+=20;
3959                    // curTn.width+=10;
3960                    curTn.top += 30;
3961                    curTn.width -= 20;
3962                    curTn.height -= 20;
3963                  }
3964                }
3965                else {
3966                  // not hovered row
3967                  if( curTn.row == 0 ) {
3968                    // first row
3969                  }
3970                  else {
3971                    curTn.top += newTop;
3972                  }
3973                }
3974              }
3975            }
3976          }
3977        }
3978      }
3979      
3980      G.GOM.displayArea.width = areaWidth;
3981      return true;
3982    }    
3983    
3984
3985    //----- MOSAIC LAYOUT
3986    // Grid using a user defined pattern layout
3987    // With this layout, a pattern definition is handeld a row
3988    function GallerySetLayoutMosaic() {
3989      var areaWidth =     G.GOM.cache.areaWidth;
3990      var gutterHeight =  G.tn.opt.Get('gutterHeight');
3991      var gutterWidth =   G.tn.opt.Get('gutterWidth');
3992      var borderWidth =   G.tn.borderWidth * 2;
3993      var borderHeight =  G.tn.borderHeight * 2;
3994
3995      var nbTn = G.GOM.items.length;
3996      var row = 0;
3997      var h = 0;
3998      var n = 0;
3999      
4000      
4001      // first loop: evaluate the gallery width based on the first row
4002      var nbCols = 0;
4003      var maxW = 0;
4004      var mosaicPattern = G.tn.settings.getMosaic();
4005      for( var i = 0; i < nbTn ; i++ ) {
4006        var curPatternElt = mosaicPattern[n];
4007
4008        var cLeft = (curPatternElt.c - 1) * G.tn.defaultSize.getOuterWidth() + (curPatternElt.c - 1) * gutterWidth;
4009        var cWidth = curPatternElt.w * G.tn.defaultSize.getOuterWidth() + (curPatternElt.w - 1) * gutterWidth;
4010        
4011        maxW=Math.max(maxW, cLeft + cWidth );
4012        
4013        nbCols=Math.max(nbCols, (curPatternElt.c - 1) + curPatternElt.w );
4014
4015        n++;
4016        if( n >= mosaicPattern.length ) {
4017          // end of pattern
4018          break;
4019        }
4020      }
4021      var totalGutterWidth = (nbCols - 1) * gutterWidth;
4022      var scaleFactor = Math.min( (areaWidth - totalGutterWidth ) / ( maxW - totalGutterWidth ), 1);
4023      
4024      // second loop: position all the thumbnails based on the layout pattern
4025      row = 0;
4026      n = 0;
4027      var mosaicPattern = G.tn.settings.getMosaic();
4028      for( var i = 0; i < nbTn ; i++ ) {
4029        var curTn = G.GOM.items[i];
4030        var curPatternElt = mosaicPattern[n];
4031        
4032        curTn.top = (curPatternElt.r - 1) * G.tn.defaultSize.getOuterHeight()*scaleFactor + (curPatternElt.r - 1) * gutterHeight + row * h + (G.tn.labelHeight.get()*(curPatternElt.r-1)) ;
4033        if( row > 0 ) {
4034          curTn.top += gutterHeight;
4035        }
4036
4037        curTn.left = (curPatternElt.c - 1) * G.tn.defaultSize.getOuterWidth()*scaleFactor + (curPatternElt.c - 1) * gutterWidth;
4038
4039        curTn.height = curPatternElt.h * G.tn.defaultSize.getOuterHeight() * scaleFactor + (curPatternElt.h - 1) * gutterHeight + (G.tn.labelHeight.get() * curPatternElt.h);
4040        curTn.resizedContentHeight = curTn.height - G.tn.labelHeight.get() - borderHeight;
4041
4042        curTn.width = curPatternElt.w * G.tn.defaultSize.getOuterWidth()*scaleFactor + (curPatternElt.w - 1) * gutterWidth;
4043        curTn.resizedContentWidth = curTn.width - borderWidth ;
4044
4045        curTn.row = row;
4046        if( row == 0 ) {
4047          h=Math.max(h, curTn.top + curTn.height);
4048        }
4049
4050        n++;
4051        if( n >= mosaicPattern.length ) {
4052          // end pattern -> new line
4053          n = 0;
4054          row++;
4055        }
4056      }
4057      
4058      G.GOM.displayArea.width = (maxW - totalGutterWidth) * scaleFactor + totalGutterWidth;
4059      return true;
4060    }
4061    
4062    
4063    
4064    // --- GRID LAYOUT
4065    function GallerySetLayoutGrid() {
4066      var curPosX=      0,
4067      curPosY=          0,   
4068      areaWidth=        G.GOM.cache.areaWidth,
4069      gutterWidth=      0,
4070      gutterHeight=     G.tn.opt.Get('gutterHeight'),
4071      maxCol=           NbThumbnailsPerRow(areaWidth),
4072      w=                0,
4073      cols=             [],
4074      curCol=           0,
4075      newAreaWidth =    areaWidth,
4076      tnWidth=          G.tn.defaultSize.getOuterWidth();
4077      var scaleFactor = 1;
4078      var nbTn=         G.GOM.items.length;
4079      var borderWidth = G.tn.borderWidth * 2;
4080      var borderHeight =G.tn.borderHeight * 2;
4081      
4082      // retrieve gutter width
4083      if( G.O.thumbnailAlignment == 'justified' ) {
4084        maxCol = Math.min( maxCol, nbTn);
4085        gutterWidth = (maxCol==1 ? 0 : (areaWidth-(maxCol*tnWidth))/(maxCol-1));
4086      }
4087      else {
4088        gutterWidth = G.tn.opt.Get('gutterWidth');
4089      }
4090
4091      // first loop to retrieve the real used width of the area (the evaluation is based on the content of the first line)
4092      // Retrieve the real used width of the area (the evaluation is based on the content of the first line)
4093      if( G.O.RTL || G.O.thumbnailAlignment == 'fillWidth' ) {
4094        // scaled --> evaluate scale factor and number of columns
4095        var totalGutterWidth = (maxCol-1) * gutterWidth;
4096        scaleFactor = (areaWidth - totalGutterWidth) / (maxCol*tnWidth);
4097        if( scaleFactor > 1 ) {
4098          maxCol++; // add one column and re-evaluate the scale factor
4099        }
4100        totalGutterWidth = (maxCol-1) * gutterWidth;
4101        scaleFactor = Math.min( (areaWidth - totalGutterWidth) / (maxCol*tnWidth), 1);   // no upscale
4102        newAreaWidth = (maxCol*tnWidth) + totalGutterWidth;
4103      }
4104
4105      
4106      G.GOM.lastFullRow = 0 ;    // display at leat 1 row (even if not full)
4107      var lastPosY = 0;
4108      var row = 0;
4109      
4110      tnWidth = tnWidth * scaleFactor;
4111      var contentWidth = tnWidth - borderWidth;
4112      var tnHeight = G.tn.defaultSize.getOuterHeight() * scaleFactor + G.tn.labelHeight.get();
4113      var contentHeight = G.tn.defaultSize.getOuterHeight() * scaleFactor - borderHeight;
4114      
4115      // loop to position and to set size of all thumbnails
4116      for( var i = 0; i < nbTn ; i++ ) {
4117        if( curPosY == 0 ) {
4118          curPosX = curCol * (tnWidth + gutterWidth)
4119          cols[curCol] = curPosX;
4120          w = curPosX + tnWidth;
4121        }
4122        else {
4123          curPosX = cols[curCol];
4124        }
4125
4126        var x = curPosX;
4127        if( G.O.RTL ) {
4128          x = parseInt(newAreaWidth) - curPosX - tnWidth;
4129        }
4130        
4131        // MANDATORY : set thumbnail position AND size
4132        var curTn=G.GOM.items[i];
4133        curTn.top = curPosY;
4134        curTn.left = x;
4135        curTn.height = tnHeight;
4136        curTn.width = tnWidth;
4137        // image size
4138        if( G.O.thumbnailAlignment == 'fillWidth' ) {
4139          curTn.resizedContentWidth = contentWidth;
4140          curTn.resizedContentHeight = contentHeight;
4141        }
4142        curTn.row = row;
4143        lastPosY = curPosY;
4144
4145        curCol++;
4146        if( curCol >= maxCol ){
4147          // new line
4148          curCol = 0;
4149          curPosY += tnHeight + gutterHeight;
4150          G.GOM.lastFullRow = row;
4151          row++;
4152        }
4153      }
4154      G.GOM.displayArea.width = w;
4155      return true;
4156    }
4157
4158
4159    //----- Display the thumbnails according to the calculated layout
4160    function GalleryDisplayPart1( forceTransition ) {
4161      if( G.CSStransformName == null ) {
4162        G.$E.conTn.css( 'left' , '0px');
4163      }
4164      else {
4165        G.$E.conTn.css( G.CSStransformName , 'none');
4166      }
4167      CacheViewport();
4168    }
4169    
4170    function CacheViewport() {
4171      G.GOM.cache.viewport = getViewport();
4172      G.GOM.cache.areaWidth = G.$E.conTnParent.width();
4173      G.GOM.cache.containerOffset = G.$E.conTnParent.offset();
4174    }
4175
4176
4177    
4178    function GalleryDisplayPart2( forceTransition ) {
4179
4180      var nbTn = G.GOM.items.length;
4181      G.GOM.itemsDisplayed = 0;
4182      var threshold = 50;
4183      var cnt = 0;    // counter for delay between each thumbnail display
4184      
4185
4186      GalleryRenderGetInterval();
4187      
4188      for( var i = 0; i < nbTn ; i++ ) {
4189        var curTn = G.GOM.items[i];
4190        if( i >= G.GOM.displayInterval.from && cnt < G.GOM.displayInterval.len ) {
4191          curTn.inDisplayArea = true;
4192          if( forceTransition ) {
4193            curTn.neverDisplayed = true;
4194          }
4195          G.GOM.itemsDisplayed++;
4196          cnt++;
4197        }
4198        else{
4199          curTn.inDisplayArea = false;
4200        }
4201      }
4202
4203      // bottom of the gallery (pagination, more button...)
4204      GalleryBottomManage();
4205
4206      var tnToDisplay = [];
4207      var tnToReDisplay = [];
4208      
4209      G.GOM.clipArea.top = -1;
4210      cnt = 0 ;
4211      var lastTnIdx = -1;
4212      G.GOM.clipArea.height = 0;
4213      // NOTE: loop always the whole GOM.items --> in case an already displayed thumbnail needs to be removed
4214      for( var i = 0; i < nbTn ; i++ ) {
4215        var curTn = G.GOM.items[i];
4216        if( curTn.inDisplayArea ) {
4217          if( G.GOM.clipArea.top == -1 ) {
4218            G.GOM.clipArea.top = curTn.top;
4219          }
4220          if( (curTn.top - G.GOM.clipArea.top) <= -1 ) {
4221            // with mosaic layout, the first thumbnail may not give the top position
4222            G.GOM.clipArea.top = curTn.top;
4223          }
4224
4225          G.GOM.clipArea.height = Math.max( G.GOM.clipArea.height, curTn.top-G.GOM.clipArea.top + curTn.height);
4226        
4227          if( curTn.neverDisplayed ) {
4228            // thumbnail is not displayed -> check if in viewport to display or not
4229            var top = G.GOM.cache.containerOffset.top + (curTn.top - G.GOM.clipArea.top);
4230            // var left=containerOffset.left+curTn.left;
4231            if( (top + curTn.height) >= (G.GOM.cache.viewport.t - threshold) && top <= (G.GOM.cache.viewport.t + G.GOM.cache.viewport.h + threshold) ) {
4232              // build thumbnail
4233              var item = G.I[curTn.thumbnailIdx];
4234              if( item.$elt == null ) {
4235                ThumbnailBuild( item, curTn.thumbnailIdx, i, (i+1) == nbTn );
4236              }
4237              tnToDisplay.push({idx:i, delay:cnt});
4238              cnt++;
4239            }
4240          }
4241          else {
4242            tnToReDisplay.push({idx: i, delay: 0});
4243          }
4244          // G.GOM.itemsDisplayed++;
4245          lastTnIdx = i;
4246        }
4247        else {
4248          curTn.displayed = false;
4249          var item = G.I[curTn.thumbnailIdx];
4250          if( item.$elt != null ){
4251            item.$elt.css({ opacity: 0, display: 'none' });
4252          }
4253        }
4254      }
4255
4256      var areaWidth = G.$E.conTnParent.width();
4257
4258      // set gallery area really used size
4259      // if( G.GOM.displayArea.width != G.GOM.displayAreaLast.width || G.GOM.displayArea.height != G.GOM.displayAreaLast.height ) {
4260      if( G.GOM.displayArea.width != G.GOM.displayAreaLast.width || G.GOM.clipArea.height != G.GOM.displayAreaLast.height ) {
4261        G.$E.conTn.width( G.GOM.displayArea.width ).height( G.GOM.clipArea.height );
4262        G.GOM.displayAreaLast.width = G.GOM.displayArea.width;
4263        G.GOM.displayAreaLast.height = G.GOM.clipArea.height;
4264        // G.GOM.displayAreaLast.height=G.GOM.displayArea.height-G.GOM.clipArea.top;
4265      }
4266
4267      if( areaWidth != G.$E.conTnParent.width() ) {
4268        // gallery area width changed since layout calculation (for example when a scrollbar appeared)
4269        // so we need re-calculate the layout before displaying the thumbnails
4270        G.GOM.cache.areaWidth = G.$E.conTnParent.width();
4271        GallerySetLayout();
4272        GalleryDisplayPart1( forceTransition );
4273        GalleryDisplayPart2( forceTransition );
4274        return;
4275      }
4276
4277      // counter of not displayed images (is displayed on the last thumbnail)
4278      if( G.layout.support.rows ) {
4279        if( G.galleryDisplayMode.Get() == 'ROWS' || (G.galleryDisplayMode.Get() == 'FULLCONTENT' && G.galleryLastRowFull.Get() && G.GOM.lastFullRow != -1) ){
4280          if( lastTnIdx < (nbTn - 1) ) {
4281            G.GOM.lastDisplayedIdxNew = lastTnIdx;
4282          }
4283          else {
4284            G.GOM.lastDisplayedIdxNew =- 1;
4285          }
4286          // remove last displayed counter
4287          if( G.GOM.lastDisplayedIdx != -1 ) {
4288            var item = G.I[G.GOM.items[G.GOM.lastDisplayedIdx].thumbnailIdx];
4289            item.$getElt('.nGY2GThumbnailIconsFullThumbnail').html('');
4290          }
4291        }
4292      }
4293
4294      
4295      // batch set position (and display animation) to all thumbnails
4296      // first display newly built thumbnails
4297      var nbBuild = tnToDisplay.length;
4298      G.GOM.thumbnails2Display=[];
4299      for( var i = 0; i < nbBuild ; i++ ) {
4300        // ThumbnailSetPosition(tnToDisplay[i].idx, tnToDisplay[i].delay+10);
4301        ThumbnailSetPosition(tnToDisplay[i].idx, i);
4302      }
4303      
4304      // then re-position already displayed thumbnails
4305      var n = tnToReDisplay.length;
4306      for( var i = 0; i < n ; i++ ) {
4307        // ThumbnailSetPosition(tnToReDisplay[i].idx, nbBuild+1);
4308        ThumbnailSetPosition(tnToReDisplay[i].idx, i);
4309      }
4310      
4311      ThumbnailDisplayAnimBatch();
4312
4313      if( G.tn.opt.Get('displayTransition') == 'NONE' ) {
4314        G.galleryResizeEventEnabled = true;
4315        // GalleryThumbnailSliderBuildAndStart();  // image slider on last displayed thumbnail
4316        TriggerCustomEvent('galleryDisplayed');
4317      }
4318      else {
4319        // setTimeout(function() {
4320        requestTimeout( function() {
4321          // change value after the end of the display transistion of the newly built thumbnails
4322          G.galleryResizeEventEnabled = true;
4323          // GalleryThumbnailSliderBuildAndStart();  // image slider on last displayed thumbnail
4324          TriggerCustomEvent('galleryDisplayed');
4325        }, nbBuild * G.tn.opt.Get('displayInterval'));
4326      }
4327      
4328    }
4329    
4330    
4331    // Thumbnail: set the new position
4332    function ThumbnailSetPosition( GOMidx, cnt ) {
4333      var newTop=   0;
4334      var curTn=    G.GOM.items[GOMidx];
4335      var idx=      G.GOM.items[GOMidx].thumbnailIdx;
4336      var item=     G.I[idx];
4337    
4338      if( curTn.neverDisplayed ) {
4339        // thumbnail is built but has never been displayed (=first display)
4340        var top = curTn.top - G.GOM.clipArea.top;
4341        if( G.tn.opt.Get('stacks') > 0 ) {
4342          // we have stacks -> do not display them here. They will be displayed at the end of the display animation
4343          item.$elt.last().css({ display: 'block'});
4344          item.$elt.css({ top: top , left: curTn.left });
4345        }
4346        else {
4347          item.$elt.css({ display: 'block', top: top , left: curTn.left });
4348        }
4349        newTop=top;
4350        
4351        // display the image of the thumbnail when fully loaded
4352        if( G.O.thumbnailWaitImageLoaded === true ) {
4353          var gi_imgLoad = ngimagesLoaded( item.$getElt('.nGY2TnImg2') );
4354          gi_imgLoad.on( 'progress', function( instance, image ) {
4355            if( image.isLoaded ) {
4356              var albumIdx = image.img.getAttribute('data-albumidx');
4357              if( albumIdx == G.GOM.albumIdx ) {
4358                // ignore event if not on current album
4359                var idx = image.img.getAttribute('data-idx');
4360                G.I[idx].ThumbnailImageReveal();
4361              }
4362            }
4363          });
4364        }
4365        // display the thumbnail
4366        ThumbnailAppear(GOMidx, cnt);
4367
4368        curTn.displayed = true;
4369        curTn.neverDisplayed = false;
4370      }
4371      else {
4372        var topOld = G.GOM.cache.containerOffset.top + item.top;
4373        var top = G.GOM.cache.containerOffset.top + (curTn.top - G.GOM.clipArea.top);
4374        newTop = curTn.top - G.GOM.clipArea.top;
4375        var vp = G.GOM.cache.viewport;
4376        if( G.O.thumbnailDisplayOutsideScreen || ( ( (topOld + curTn.height) >= (vp.t - vp.h) && topOld <= (vp.t + vp.h * 4) ) ||
4377              ( (top + curTn.height) >= (vp.t - vp.h) && top <= (vp.t + vp.h * 4) ) )  ) {
4378          // thumbnail positioned in enlarged viewport (viewport + 4 x viewport height) (v1.5: changed from 2 to 4)
4379          if( curTn.displayed ) {
4380            // thumbnail is displayed
4381            if( item.top != curTn.top || item.left != curTn.left ) {
4382              // set position
4383              if( G.O.galleryResizeAnimation == true ) {
4384                // with transition
4385                var tweenable = new NGTweenable();
4386                tweenable.tween({
4387                  from:       { top: item.top, left: item.left,  height: item.height,  width: item.width },
4388                  to:         { top: newTop,   left: curTn.left, height: curTn.height, width: curTn.width },
4389                  attachment: { $e: item.$elt },
4390                  duration:   100,
4391                  delay:      cnt * G.tn.opt.Get('displayInterval') / 5,
4392                  // easing:     'easeInOutQuad',
4393                  easing:     'easeOutQuart',
4394                  step:       function (state, att) {
4395                    // window.ng_draf( function() {
4396                      att.$e.css(state);
4397                    // });
4398                  },
4399                  finish:     function (state, att) {
4400                    var _this=this;
4401                    // window.ng_draf( function() {
4402                      _this.dispose();
4403                    // });
4404                  }
4405                });
4406              }
4407              else {
4408                // set position without transition
4409                // item.$elt.css({ top: curTn.top , left: curTn.left });
4410                item.$elt.css({ top: newTop , left: curTn.left });
4411              }
4412            }
4413          }
4414          else {
4415            // re-display thumbnail
4416            curTn.displayed = true;
4417            // item.$elt.css({ display: 'block', top: curTn.top , left: curTn.left, opacity:1 });
4418            item.$elt.css({ display: 'block', top: newTop, left: curTn.left, opacity: 1 });
4419            ThumbnailAppearFinish(item);
4420          }
4421        }
4422        else {
4423          // undisplay thumbnail if not in viewport+margin --> performance gain
4424          curTn.displayed = false;
4425          item.$elt.css({ display: 'none'});
4426        }
4427      }
4428      item.left = curTn.left;
4429      item.top = newTop;
4430      
4431      // set new size if changed
4432      if( item.width != curTn.width || item.height != curTn.height ) {
4433        item.$elt.css({ width: curTn.width , height: curTn.height });
4434        item.width = curTn.width;
4435        item.height = curTn.height;
4436        
4437        // if( curTn.resizedContentWidth > 0 ) {
4438        // resize also the content (=image)
4439        if( item.resizedContentWidth != curTn.resizedContentWidth || item.resizedContentHeight != curTn.resizedContentHeight ) {
4440          if( item.kind == 'albumUp' ) {
4441            // item.$getElt('.nGY2GThumbnailAlbumUp').css({'height': curTn.resizedContentHeight, 'width': curTn.resizedContentWidth});
4442          }
4443          else {
4444            item.$getElt('.nGY2GThumbnailImage').css({'height': curTn.resizedContentHeight, 'width': curTn.resizedContentWidth});
4445
4446            if( G.layout.engine == 'JUSTIFIED'  ) {
4447              item.$getElt('.nGY2GThumbnailImg').css({'height': curTn.resizedContentHeight, 'width': curTn.resizedContentWidth});
4448            }
4449          }
4450          item.resizedContentWidth = curTn.resizedContentWidth;
4451          item.resizedContentHeight = curTn.resizedContentHeight;
4452        }
4453      }
4454      
4455      
4456      // add counter of remaining (not displayed) images 
4457      if( G.GOM.lastDisplayedIdxNew == GOMidx &&  G.layout.support.rows ) {
4458        if( (G.galleryDisplayMode.Get() == 'ROWS' && G.galleryMaxRows.Get() > 0) || (G.galleryDisplayMode.Get() == 'FULLCONTENT' && G.galleryLastRowFull.Get() && G.GOM.lastFullRow != -1) ){
4459          // number of items
4460          var nb = G.GOM.items.length - GOMidx - 1;
4461          if( item.albumID != '0' && G.O.thumbnailLevelUp ) {
4462            nb--;
4463          }
4464
4465          if( nb > 0 ) {
4466            // display counter
4467            if( G.O.thumbnailOpenImage || G.O.thumbnailSliderDelay > 0  ) {
4468              item.$getElt('.nGY2GThumbnailIconsFullThumbnail').html( '+' + nb);
4469            }
4470
4471            // if( G.layout.engine == 'GRID' && G.GOM.slider.hostItem != G.GOM.NGY2Item(GOMidx) ) {
4472            // image slider on last displayed thumbnail
4473            if( G.GOM.slider.hostItem != G.GOM.NGY2Item(GOMidx) ) {
4474
4475              // set current slider back to initial content
4476              GalleryThumbnailSliderSetContent( G.GOM.slider.hostItem );
4477              // new slider
4478              G.GOM.slider.hostIdx = GOMidx;
4479              G.GOM.slider.hostItem = G.GOM.NGY2Item(GOMidx);
4480              G.GOM.slider.nextIdx = GOMidx;
4481              G.GOM.slider.currentIdx = GOMidx;
4482              GalleryThumbnailSliderBuildAndStart();  // image slider on last displayed thumbnail
4483              // GalleryThumbnailSliderSetNextContent();
4484            }
4485          }
4486          else {
4487            // reset slider content to initial content because all thumbnails are displayed
4488            GalleryThumbnailSliderSetContent( G.GOM.slider.hostItem );
4489            G.GOM.slider.hostIdx = -1;
4490          }
4491          
4492          G.GOM.lastDisplayedIdx = GOMidx;
4493        }
4494      }
4495
4496    }
4497    
4498    // ---------------------
4499    // replace image on last thumbnails with not displayed ones (mode ROWS or FULLCONTENT with galleryLastRowFull enabled)
4500    // function GalleryLastThumbnailSlideImage() {
4501    function GalleryThumbnailSliderBuildAndStart() {
4502
4503      if( G.O.thumbnailSliderDelay == 0 || G.GOM.slider.hostIdx == -1 ) {
4504        return;
4505      }
4506      clearTimeout(G.GOM.slider.timerID);
4507      
4508      var item = G.GOM.slider.hostItem;
4509
4510      // dupplicate image layer -> for the next image
4511      if( item.$getElt('.nGY2TnImgNext').length == 0 ) {
4512        item.$getElt('.nGY2TnImg').clone().removeClass('nGY2TnImg').addClass('nGY2TnImgNext').insertAfter(item.$getElt('.nGY2TnImg'));
4513        item.$getElt('.nGY2TnImgBack').clone().removeClass('nGY2TnImgBack').addClass('nGY2TnImgBackNext').insertAfter(item.$getElt('.nGY2TnImg', true));
4514        item.$getElt('.nGY2GThumbnailImage', true); // important -> refresh the cache
4515        item.$getElt('.nGY2GThumbnailImg', true);   // important -> refresh the cache
4516      }
4517
4518      item.CSSTransformSet('.nGY2TnImgNext', 'translateX', '100%', true);
4519      item.CSSTransformApply( '.nGY2TnImgNext' );
4520      item.CSSTransformSet('.nGY2TnImgBackNext', 'translateX', '100%', true);
4521      item.CSSTransformApply( '.nGY2TnImgBackNext' );
4522
4523      GalleryThumbnailSliderSetNextContent();
4524      
4525      // clearTimeout(G.GOM.slider.timerID);
4526      // G.GOM.slider.timerID = setTimeout(function(){ GalleryThumbnailSliderStartTransition() }, G.O.thumbnailSliderDelay);
4527      G.GOM.slider.timerID = requestTimeout(function(){ GalleryThumbnailSliderStartTransition() }, G.O.thumbnailSliderDelay);
4528    }
4529
4530    
4531    function GalleryThumbnailSliderSetNextContent() {
4532
4533      G.GOM.slider.nextIdx++;
4534      if( G.GOM.slider.nextIdx >= G.GOM.items.length ) {
4535        G.GOM.slider.nextIdx = G.GOM.slider.hostIdx;
4536      }
4537      
4538      // new image
4539      var newItem = G.GOM.NGY2Item(G.GOM.slider.nextIdx);
4540      var imgBlurred = G.emptyGif;
4541      var bgImg = "url('" + G.emptyGif + "')";
4542      if( newItem.imageDominantColors != null ) {
4543        imgBlurred = newItem.imageDominantColors;
4544        bgImg = "url('" + newItem.imageDominantColors + "')";
4545      }
4546      G.GOM.slider.hostItem.$getElt('.nGY2TnImgBackNext', true).css({'background-image': bgImg, opacity: 1 });
4547      G.GOM.slider.hostItem.$getElt('.nGY2TnImgNext', true).css({ 'background-image': "url('" + newItem.thumbImg().src + "')", opacity: 1 });
4548      G.GOM.slider.hostItem.$getElt('.nGY2TnImgNext .nGY2GThumbnailImg', true).attr('src', newItem.thumbImg().src );
4549      
4550
4551    }
4552    
4553    // thumbnail slider - transition from one image to the next one
4554    function GalleryThumbnailSliderStartTransition() {
4555      
4556      if( G.GOM.slider.hostItem.$getElt() != null ) {
4557
4558        // slider transition
4559        var tweenable = new NGTweenable();
4560        G.GOM.slider.tween = tweenable;
4561        tweenable.tween({
4562          from:         { 'left': 100 },
4563          to:           { 'left': 0 },
4564          duration:     800,
4565          delay:        0,
4566          // easing:       'easeInOutQuad',
4567          easing:       'easeOutQuart',
4568          
4569          step: function (state) {
4570            if( G.GOM.slider.hostItem.$getElt() == null ) {
4571              // the thumbnail may have been destroyed since the start of the animation
4572              G.GOM.slider.tween.stop(false);
4573              return;
4574            }
4575
4576            // window.ng_draf( function() {
4577              // slide current content
4578              G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImgBack', 'translateX', -(100 - state.left) + '%');
4579              G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImgBack' );
4580              G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImg', 'translateX', -(100 - state.left) + '%');
4581              G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImg' );
4582
4583              // slide new content
4584              G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImgBackNext', 'translateX', state.left + '%');
4585              G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImgBackNext' );
4586              G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImgNext', 'translateX', state.left + '%');
4587              G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImgNext' );
4588            // });
4589
4590            
4591          },
4592          finish: function (state) {
4593            if( G.GOM.slider.hostItem.$getElt() == null ) {
4594              // the thumbnail may be destroyed since the start of the animation
4595              return;
4596            }
4597           
4598            if( G.GOM.NGY2Item(G.GOM.slider.nextIdx) == null ) { return; } // item does not exist anymore
4599            
4600            // window.ng_draf( function() {
4601              // set new content as current content
4602              GalleryThumbnailSliderSetContent( G.GOM.NGY2Item(G.GOM.slider.nextIdx) );
4603              G.GOM.slider.currentIdx = G.GOM.slider.nextIdx;
4604              GalleryThumbnailSliderSetNextContent();
4605              
4606              clearTimeout(G.GOM.slider.timerID);
4607              // G.GOM.slider.timerID=setTimeout(function(){ GalleryThumbnailSliderStartTransition() }, G.O.thumbnailSliderDelay);
4608              G.GOM.slider.timerID = requestTimeout(function(){ GalleryThumbnailSliderStartTransition() }, G.O.thumbnailSliderDelay);
4609            // });
4610          }
4611        });
4612      }
4613    }
4614    
4615    // set main content of the thumbnail hosting the slider
4616    // hide the elements for the next content of the slider
4617    function GalleryThumbnailSliderSetContent( ngy2itemContent ) {
4618        if( G.GOM.slider.hostIdx == -1 ) { return; }
4619        
4620        if( G.GOM.slider.tween != null ) {
4621          if( G.GOM.slider.tween._isTweening  == true ) {
4622            G.GOM.slider.tween.stop(false);
4623          }
4624        }
4625
4626        var bgImg = "url('" + G.emptyGif + "')";
4627        if( ngy2itemContent.imageDominantColors != null ) {
4628          bgImg = "url('" + ngy2itemContent.imageDominantColors + "')";
4629        }
4630        G.GOM.slider.hostItem.$getElt('.nGY2TnImgBack').css('background-image', bgImg);
4631        G.GOM.slider.hostItem.$getElt('.nGY2TnImg').css('background-image', "url('" + ngy2itemContent.thumbImg().src + "')" );
4632        G.GOM.slider.hostItem.$getElt('.nGY2TnImg .nGY2GThumbnailImg').attr('src', ngy2itemContent.thumbImg().src );
4633        
4634        G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImgBack', 'translateX', '0');
4635        G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImgBack' );
4636        G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImg', 'translateX', '0');
4637        G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImg' );
4638
4639        // place the containers for the next image slider outside of the thumbnail (=hidden)
4640        G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImgBackNext', 'translateX', '100%', true);
4641        G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImgBackNext' );
4642        G.GOM.slider.hostItem.CSSTransformSet('.nGY2TnImgNext', 'translateX', '100%', true);
4643        G.GOM.slider.hostItem.CSSTransformApply( '.nGY2TnImgNext' );
4644
4645        // set new title and description
4646        if( G.O.thumbnailLabel.get('display') == true ) {
4647          var icons = G.O.icons.thumbnailAlbum;
4648          if( ngy2itemContent.kind != 'album' ) {
4649            icons = G.O.icons.thumbnailImage;
4650          }
4651          G.GOM.slider.hostItem.$getElt('.nGY2GThumbnailTitle').html(icons + getThumbnailTitle(ngy2itemContent));
4652        //   G.GOM.slider.hostItem.$getElt('.nGY2GThumbnailDescription').html(icons + getTumbnailDescription(ngy2itemContent));
4653        }
4654      }
4655    
4656   
4657    
4658    // Compute the height of the label part of a thumbnail (title+description, both single line)
4659    function ThumbnailGetLabelHeight() {
4660      var newElt = [],
4661      newEltIdx =  0;
4662
4663      // if( G.O.thumbnailLabel.get('display') == false && G.tn.toolbar.getWidth(item) <= 0 ) {
4664      if( G.O.thumbnailLabel.get('display') == false  ) {
4665        return 0;
4666      }
4667      
4668      var desc='';
4669      if( G.O.thumbnailLabel.get('displayDescription') == true ) {
4670        desc = 'aAzZjJ';
4671      }
4672
4673      // visibility set to hidden
4674      newElt[newEltIdx++] = '<div class="nGY2GThumbnail ' + G.O.theme + '" style="display:block;visibility:hidden;position:absolute;top:-9999px;left:-9999px;" ><div class="nGY2GThumbnailSub">';
4675      if( G.O.thumbnailLabel.get('display') == true ) {
4676        // Labels: title and description
4677        newElt[newEltIdx++] = '  <div class="nGY2GThumbnailLabel" '+ G.tn.style.getLabel() +'>';
4678        newElt[newEltIdx++] = '    <div class="nGY2GThumbnailAlbumTitle" '+G.tn.style.getTitle()+'>aAzZjJ</div>';
4679        if( G.O.thumbnailLabel.get('displayDescription') == true ) {
4680        //   newElt[newEltIdx++] = '    <div class="nGY2GThumbnailDescription" '+G.tn.style.getDesc()+'>'+'aAzZjJ'+'</div>';
4681        }
4682        newElt[newEltIdx++] = '  </div>';
4683      }
4684      
4685      newElt[newEltIdx++] = '</div></div>';
4686    
4687      var $newDiv = jQuery(newElt.join('')).appendTo(G.$E.conTn);
4688      var h = $newDiv.find('.nGY2GThumbnailLabel').outerHeight(true);
4689      $newDiv.remove();
4690
4691      return h;
4692    }
4693    
4694    function ThumbnailBuildStacks( bgColor ) {
4695      var ns=G.tn.opt.Get('stacks');
4696      if( ns == 0 ) { return ''; }
4697     
4698      var s='';
4699      for( var i=0; i<ns; i++ ) {
4700        s='<div class="nGY2GThumbnailStack " style="display:none;'+bgColor+'"></div>'+s;
4701      }
4702      return s;
4703    }
4704    
4705    //----- Build one UP thumbnail (=navigation thumbnail)
4706    function ThumbnailBuildAlbumpUp( item, idx, GOMidx ) {
4707      var newElt = [],
4708      newEltIdx = 0;
4709      
4710      var mp = '';
4711      if( G.O.thumbnailOpenImage === false ) {
4712        mp = 'cursor:default;'
4713      }
4714      
4715      newElt[newEltIdx++] = ThumbnailBuildStacks('') + '<div class="nGY2GThumbnail" style="display:none;opacity:0;' + mp + '" >';
4716      newElt[newEltIdx++] = '  <div class="nGY2GThumbnailSub">';
4717
4718      var h=G.tn.defaultSize.getHeight(),
4719      w=G.tn.defaultSize.getWidth();
4720
4721      newElt[newEltIdx++] = '    <div class="nGY2GThumbnailImage" style="width:'+w+'px;height:'+h+'px;"><img class="nGY2GThumbnailImg" src="'+G.emptyGif+'" alt="alt2" style="max-width:'+w+'px;max-height:'+h+'px;" ></div>';
4722      // newElt[newEltIdx++] = '    <div class="nGY2GThumbnailAlbumUp" style="width:'+w+'px;height:'+h+'px;">'+G.O.icons.thumbnailAlbumUp+'</div>';
4723      newElt[newEltIdx++] = '    <div class="nGY2GThumbnailAlbumUp" >'+G.O.icons.thumbnailAlbumUp+'</div>';
4724      newElt[newEltIdx++] = '  </div>';
4725      newElt[newEltIdx++] = '</div>';
4726      
4727      var $newDiv = jQuery(newElt.join('')).appendTo(G.$E.conTn); //.animate({ opacity: 1},1000, 'swing');  //.show('slow'); //.fadeIn('slow').slideDown('slow');
4728      
4729      item.$elt = $newDiv;
4730      $newDiv.data('index', GOMidx);
4731      item.$getElt('.nGY2GThumbnailImg').data('index', GOMidx);
4732      
4733      return;
4734    }
4735
4736    
4737    //----- Build one thumbnail
4738    function ThumbnailBuild( item, idx, GOMidx, lastOne ) {
4739      item.eltTransform =  [];
4740      item.eltFilter =     [];
4741      item.hoverInitDone = false;
4742      item.$Elts =         [];
4743
4744      if( item.kind == 'albumUp' ) {
4745        ThumbnailBuildAlbumpUp( item, idx, GOMidx);
4746        return;
4747      }
4748
4749      var newElt = [],
4750      newEltIdx = 0;
4751
4752      var mp = '';
4753      if( G.O.thumbnailOpenImage === false ) {
4754        mp = 'cursor:default;'
4755      }
4756
4757      // var src = encodeURI(item.thumbImg().src),
4758      var src = (item.thumbImg().src).replace(/'/g, "%27"),   // replace single quote with %27
4759      sTitle = getThumbnailTitle(item);
4760      var sDesc = getTumbnailDescription(item);
4761
4762
4763      // image background -> visible during image download
4764      var bg = '';
4765      var bgImg = "background-image: url('" + G.emptyGif + "');";
4766      if( item.imageDominantColors != null ) {
4767        // dominant colorS (blurred preview image)
4768        bgImg = "background-image: url('" + item.imageDominantColors + "');";
4769      }
4770      else {
4771        // dominant color -> background color
4772        if( item.imageDominantColor != null ) {
4773          bg = 'background-color:' + item.imageDominantColor + ';';
4774        }
4775        else {
4776          bgImg = '';
4777        }
4778      }
4779
4780      var op = 'opacity:1;';
4781      if( G.O.thumbnailWaitImageLoaded == true ) {
4782        op = 'opacity:0;';
4783      }
4784
4785      // ##### thumbnail containers  (with stacks)
4786      newElt[newEltIdx++] = ThumbnailBuildStacks(bg) + '<div class="nGY2GThumbnail" style="display:none;opacity:0;' + mp + '"><div class="nGY2GThumbnailSub ' + ( G.O.thumbnailSelectable && item.selected ? "nGY2GThumbnailSubSelected" : "" ) + '">';
4787
4788      
4789      // image size
4790      var w = G.tn.settings.getW();
4791      var h = G.tn.settings.getH();
4792      if( G.tn.settings.getMosaic() !== null ) {
4793        // mosaic layout -> 
4794        w = G.GOM.items[GOMidx].width;
4795        h = G.GOM.items[GOMidx].height;
4796      }
4797
4798      var bgSize = 'contain';
4799      if( G.tn.opt.Get('crop') ) {
4800        bgSize = 'cover';
4801      }
4802      
4803      // ##### layer for image background (color, dominant color, blurred preview)
4804      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;";
4805      newElt[newEltIdx++]='<div class="nGY2GThumbnailImage nGY2TnImgBack" style="' + s1 + '"></div>';
4806      
4807      // #### layer for image 
4808      var s2 = op + "position: absolute; top: 0px; left: 0px; width:" + w + "px; height:" + h + "px; background-image: url('" + src + "'); background-position: center center; background-repeat: no-repeat; background-size:" + bgSize + "; overflow: hidden;";
4809      newElt[newEltIdx++]='<div class="nGY2GThumbnailImage nGY2TnImg" style="' + s2 + '">';
4810      newElt[newEltIdx++]='  <img class="nGY2GThumbnailImg nGY2TnImg2" src="' + src + '" alt="' + sDesc + '" style="opacity:0;" data-idx="' + idx + '" data-albumidx="' + G.GOM.albumIdx + '" >';
4811      newElt[newEltIdx++]='</div>';
4812      
4813      // ##### layer for user customization purposes
4814      newElt[newEltIdx++]='<div class="nGY2GThumbnailCustomLayer"></div>';
4815
4816      // ##### layer for labels (title + description and their icons)
4817      if( G.O.thumbnailLabel.get('display') == true ) {
4818        // Labels: title and description
4819        newElt[newEltIdx++]= '  <div class="nGY2GThumbnailLabel" '+ G.tn.style.getLabel(item) + '>';
4820        if( item.kind == 'album' ) {
4821          // album kind
4822          newElt[newEltIdx++]= '    <div class="nGY2GThumbnailTitle nGY2GThumbnailAlbumTitle" ' + G.tn.style.getTitle() + '>' + G.O.icons.thumbnailAlbum + sTitle + '</div>';
4823        }
4824        else {
4825          // image/media kind
4826          newElt[newEltIdx++]= '    <div class="nGY2GThumbnailTitle nGY2GThumbnailImageTitle" ' + G.tn.style.getTitle() + '>' + G.O.icons.thumbnailImage + sTitle + '</div>';
4827        }
4828        // newElt[newEltIdx++]= '    <div class="nGY2GThumbnailDescription" ' + G.tn.style.getDesc() + '>' + getTumbnailDescription(item) + '</div>';
4829        newElt[newEltIdx++]= '  </div>';
4830      }
4831
4832      // ##### layer for tools
4833      newElt[newEltIdx++] = ThumbnailBuildTools(item, lastOne);
4834      
4835      // close containers
4836      newElt[newEltIdx++]='</div></div>';
4837      
4838      var $newDiv =jQuery(newElt.join('')).appendTo(G.$E.conTn);
4839
4840      item.$elt=$newDiv;
4841      $newDiv.data('index',GOMidx);
4842      item.$getElt('.nGY2GThumbnailImg').data('index',GOMidx);
4843      
4844      // Custom init function
4845      var fu=G.O.fnThumbnailInit;
4846      if( fu !== null ) {
4847        typeof fu == 'function' ? fu($newDiv, item, GOMidx) : window[fu]($newDiv, item, GOMidx);
4848      }
4849
4850      if( item.title != 'image gallery by nanogallery2 [build]' ) {
4851        ThumbnailOverInit(GOMidx);
4852      }
4853      
4854      return ;
4855    }
4856
4857    
4858    // Thumbnail layer for tools (toolbars and counter)
4859    function ThumbnailBuildTools( item, lastThumbnail ) {
4860    
4861      // toolbars
4862      var tb = ThumbnailBuildToolbarOne(item, 'topLeft') + ThumbnailBuildToolbarOne(item, 'topRight') + ThumbnailBuildToolbarOne(item, 'bottomLeft') + ThumbnailBuildToolbarOne(item, 'bottomRight');
4863      
4864      // counter of not displayed images
4865      tb += '<div class="nGY2GThumbnailIconsFullThumbnail"></div>';
4866
4867      return tb;
4868    }
4869    
4870    function ThumbnailBuildToolbarOne( item, position ) {
4871      var toolbar = '';
4872      var tb =      G.tn.toolbar.get(item);
4873      var width =   { xs:0, sm:1, me:2, la:3, xl:4 };
4874      var cnt =     0;
4875      
4876      if( tb[position] != '' ) {
4877        var pos='top: 0; right: 0; text-align: right;';     // 'topRight' and default
4878        switch( position ) {
4879          case 'topLeft':
4880            pos = 'top: 0; left: 0; text-align: left;';
4881            break;
4882          case 'bottomRight':
4883            pos = 'bottom: 0; right: 0; text-align: right;';
4884            break;
4885          case 'bottomLeft':
4886            pos = 'bottom: 0; left: 0; text-align: left;';
4887            break;
4888        }
4889        
4890        toolbar += '  <ul class="nGY2GThumbnailIcons" style="' + pos + '">';
4891        
4892        var icons = tb[position].split(',');
4893        var nb = icons.length;
4894        for( var i = 0; i < nb; i++ ) {
4895          var icon = icons[i].replace(/^\s*|\s*$/, '');   //trim trailing/leading whitespace
4896
4897          var minWidth = icon.substring(0,2).toLowerCase();
4898          var tIcon = icon;
4899          var display = true;
4900          if( /xs|sm|me|la|xl/i.test(minWidth) ) {
4901            // check visbility (depending on screen width)
4902            if( width[minWidth] > width[G.GOM.curWidth] ) {
4903              display = false;
4904            }
4905            tIcon = icon.substring(2);
4906          }
4907          
4908          if( display ) {
4909            var sp=(i+1<nb ? '&nbsp;' :'');
4910            switch( tIcon ) {
4911              case 'COUNTER':
4912                if( item.kind == 'album' ) {
4913                  toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="">';
4914                  toolbar += '      <div class="nGY2GThumbnailIconImageCounter"></div>';
4915                  toolbar += '      <div class="nGY2GThumbnailIconText">' + G.O.icons.thumbnailCounter+Math.max((item.getContentLength(false)),item.numberItems) + sp + '</div>';
4916                  toolbar += '    </li>';
4917                  cnt++;
4918                }
4919                break;
4920              case 'COUNTER2':
4921                if( item.kind == 'album' ) {
4922                  toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="">';
4923                  toolbar += '      <div class="nGY2GThumbnailIconTextBadge">' + G.O.icons.thumbnailCounter+Math.max((item.getContentLength(false)),item.numberItems) + sp + '</div>';
4924                  toolbar += '    </li>';
4925                  cnt++;
4926                }
4927                break;
4928              case 'SHARE':
4929                toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="' + tIcon + '">';
4930                toolbar += '      <div>' + G.O.icons.thumbnailShare + '</div>';
4931                toolbar += '    </li>';
4932                cnt++;
4933                break;
4934              case 'DOWNLOAD':
4935                toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="' + tIcon + '">';
4936                toolbar += '      <div>' + G.O.icons.thumbnailDownload + '</div>';
4937                toolbar += '    </li>';
4938                cnt++;
4939                break;
4940              case 'INFO':
4941                toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="' + tIcon + '">';
4942                toolbar += '      <div>' + G.O.icons.thumbnailInfo + '</div>';
4943                toolbar += '    </li>';
4944                cnt++;
4945                break;
4946              case 'CART':
4947                toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="' + tIcon + '">';
4948                toolbar += '      <div>' + G.O.icons.thumbnailCart + '</div>';
4949                toolbar += '    </li>';
4950                cnt++;
4951                break;
4952              case 'DISPLAY':
4953                toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="DISPLAY">';
4954                toolbar += '      <div class="nGY2GThumbnailIconImageShare">' + G.O.icons.thumbnailDisplay + '</div>';
4955                toolbar += '    </li>';
4956                cnt++;
4957                break;
4958              case 'CUSTOM1':
4959              case 'CUSTOM2':
4960              case 'CUSTOM3':
4961              case 'CUSTOM4':
4962              case 'CUSTOM5':
4963              case 'CUSTOM6':
4964              case 'CUSTOM7':
4965              case 'CUSTOM8':
4966              case 'CUSTOM9':
4967              case 'CUSTOM10':
4968                var cust = tIcon.replace('CUSTOM', '');
4969                toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="' + tIcon.toLowerCase() + '">';
4970                toolbar += '      <div class="nGY2GThumbnailIconImageShare">' + G.O.icons['thumbnailCustomTool' + cust] + '</div>';
4971                toolbar += '    </li>';
4972                cnt++;
4973                break;
4974              case 'FEATURED':
4975                if( item.featured === true ) {
4976                  toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="">';
4977                  toolbar += '      <div class="nGY2GThumbnailIconImageFeatured">' + G.O.icons.thumbnailFeatured + '</div>';
4978                  toolbar += '    </li>';
4979                  cnt++;
4980                }
4981                break;
4982              case 'SELECT':
4983                if( G.O.thumbnailSelectable == true ) {
4984                  toolbar += '    <li class="nGY2GThumbnailIcon" data-ngy2action="TOGGLESELECT">';
4985                  if( item.selected === true ) {
4986                    toolbar += '      <div class="nGY2GThumbnailIconImageSelect nGY2ThumbnailSelected">' + G.O.icons.thumbnailSelected + '</div>';
4987                  }
4988                  else {
4989                    toolbar += '      <div class="nGY2GThumbnailIconImageSelect nGY2ThumbnailUnselected">' + G.O.icons.thumbnailUnselected + '</div>';
4990                  }
4991                  toolbar += '    </li>';
4992                  cnt++;
4993                }
4994                break;
4995            }
4996          }
4997        }
4998        toolbar += '  </ul>';
4999      }
5000      
5001      if( cnt > 0 ) {
5002        return toolbar;
5003      }
5004      else {
5005        return '';
5006      }
5007    }
5008    
5009    function getThumbnailTitle( item ) {
5010    
5011      var sTitle = item.title;
5012      if( G.O.thumbnailLabel.get('display') == true ) {
5013        if( sTitle === undefined || sTitle.length == 0 ) { sTitle = '&nbsp;'; }
5014
5015        if( G.i18nTranslations.thumbnailImageTitle != '' ) {
5016          sTitle = G.i18nTranslations.thumbnailImageTitle;
5017        }
5018        var ml = G.O.thumbnailLabel.get('titleMaxLength');
5019        if( ml > 3 && sTitle.length > ml ){
5020          sTitle = sTitle.substring(0, ml) + '...';
5021        }
5022      }
5023      
5024      return sTitle;
5025    }
5026
5027    function getTumbnailDescription( item ) {
5028      var sDesc = '';
5029      sDesc = item.description;
5030      if( G.O.thumbnailLabel.get('displayDescription') == true ) { 
5031        if( item.kind == 'album' ) {
5032          if( G.i18nTranslations.thumbnailImageDescription != '' ) {
5033            sDesc = G.i18nTranslations.thumbnailAlbumDescription;
5034          }
5035          else {
5036            sDesc = item.description;
5037          }
5038        }
5039        else {
5040          if( G.i18nTranslations.thumbnailImageDescription != '' ) {
5041            sDesc = G.i18nTranslations.thumbnailImageDescription;
5042          }
5043          else {
5044            sDesc = item.description;
5045          }
5046        }
5047        var ml = G.O.thumbnailLabel.get('descriptionMaxLength');
5048        if( ml > 3 && sDesc.length > ml ){
5049          sDesc = sDesc.substring(0, ml) + '...';
5050        }
5051        if( sDesc.length == 0 ) {
5052          sDesc = '&nbsp;';
5053        }
5054      }
5055      if(sDesc == '') {
5056        sDesc = getThumbnailTitle(item);
5057      }
5058      
5059      return sDesc;
5060    }
5061
5062    
5063    
5064    // Retrieve the maximum number of thumbnails that fits in one row
5065    function NbThumbnailsPerRow( areaWidth ) {
5066      var tnW = G.tn.defaultSize.getOuterWidth();
5067      
5068      var nbMaxTn = 0;
5069      if( G.O.thumbnailAlignment == 'justified' ) {
5070        nbMaxTn = Math.floor((areaWidth)/(tnW));
5071      }
5072      else {
5073        nbMaxTn = Math.floor((areaWidth + G.tn.opt.Get('gutterWidth'))/(tnW + G.tn.opt.Get('gutterWidth')));
5074      }
5075      
5076      if(  G.O.maxItemsPerLine >0 && nbMaxTn >  G.O.maxItemsPerLine ) {
5077        nbMaxTn = G.O.maxItemsPerLine;
5078      }
5079      
5080      if( nbMaxTn < 1 ) { nbMaxTn = 1; }
5081      
5082      return nbMaxTn
5083    }
5084  
5085    // Thumbnail display animation
5086    function ThumbnailAppear( n, cnt ) {
5087      var curTn = G.GOM.items[n];
5088      var item = G.I[curTn.thumbnailIdx];
5089
5090    
5091      if( G.tn.opt.Get('displayTransition') == 'NONE' ) {
5092        item.$elt.css({ opacity: 1 });
5093        ThumbnailAppearFinish( item );
5094      }
5095      else {
5096        if( item.$elt == null ) { return; }
5097        var top = G.GOM.cache.containerOffset.top + ( curTn.top - G.GOM.clipArea.top );
5098        var vp = G.GOM.cache.viewport;
5099        if( (top + (curTn.top - G.GOM.clipArea.top)) >= (vp.t - 50) && top <= (vp.t + vp.h + 50) ) {
5100          // display animation only if in the current viewport
5101          var delay = cnt * G.tn.opt.Get('displayInterval');
5102          if( G.tn.opt.Get('displayTransition') == 'CUSTOM' ) {
5103            if( G.GOM.curNavLevel == 'lN' ) {
5104              G.O.fnThumbnailDisplayEffect(item.$elt, item, n, delay);
5105            }
5106            else {
5107              G.O.fnThumbnailL1DisplayEffect(item.$elt, item, n, delay);
5108            }
5109          }
5110          else {
5111            G.GOM.thumbnails2Display.push({itm: item, d: delay});
5112            // ThumbnailDisplayAnim2(item, delay);
5113          }
5114          return;
5115        }
5116        else {
5117          item.$elt.css({ opacity: 1 });
5118          ThumbnailAppearFinish(item);
5119        }
5120      }
5121    }
5122    
5123    
5124    // displays thumbnail stacks at the end of the display animation
5125    function ThumbnailAppearFinish( item ) {
5126    
5127      // add stacks
5128      var ns = G.tn.opt.Get('stacks');
5129      if( ns > 0 ) {
5130        // display stacks
5131        item.$elt.css({ display: 'block'});
5132        var o = 0.9;
5133        // set stack opacity
5134        for( var i = ns-1; i>=0; i-- ) {
5135          item.$elt.eq(i).css('opacity', o);
5136          o = o - 0.2;
5137        }
5138
5139      }
5140    }
5141    
5142
5143    function ThumbnailDisplayAnim2( item, delay ) {
5144      function randomIntFromInterval(min,max) {
5145        return Math.floor(Math.random()*(max-min+1)+min);
5146      }
5147      var oFrom = {};
5148      var oTo = {};
5149    
5150      switch (G.tn.opt.Get('displayTransition')) {
5151        case 'RANDOMSCALE':
5152          var scales = [0.95, 1, 1.05, 1.1];
5153          var zi = [1, 2, 3, 4];
5154          
5155          var r = randomIntFromInterval(0,3);
5156          while( r == G.GOM.lastRandomValue ) {
5157            r = randomIntFromInterval(0,3);
5158          }
5159          G.GOM.lastRandomValue = r;
5160          var f = scales[r];
5161          // item.$elt.css({ 'z-index': G.GOM.lastZIndex+zi[r], 'box-shadow': '-1px 2px 5px 1px rgba(0, 0, 0, 0.7)' });
5162          item.$elt.css({ 'z-index': G.GOM.lastZIndex+zi[r], 'box-shadow': '0px 0px 5px 3px rgba(0,0,0,0.74)' });
5163          
5164          oFrom = { scale: 0.5, opacity:0 };
5165          oTo =   { scale: f,   opacity:1 };
5166          break;
5167
5168        case 'SCALEUP':
5169          var f = G.tn.opt.Get('displayTransitionStartVal');
5170          if( f == 0 ) { f = 0.6; }   // default value
5171          oFrom = { scale: f, opacity: 0 };
5172          oTo =   { scale: 1, opacity: 1 };
5173          break;
5174
5175        case 'SCALEDOWN':
5176          var f = G.tn.opt.Get('displayTransitionStartVal');
5177          if( f == 0 ) { f=1.3; }   // default value
5178          oFrom = { scale: f, opacity: 0 };
5179          oTo =   { scale: 1, opacity: 1 };
5180          break;
5181        case 'SLIDEUP':
5182          var f = G.tn.opt.Get('displayTransitionStartVal');
5183          if( f == 0 ) { f=50; }   // default value
5184          oFrom = { opacity: 0, translateY: f };
5185          oTo =   { opacity: 1, translateY: 0 };
5186          break;
5187        case 'SLIDEDOWN':
5188          var f=G.tn.opt.Get('displayTransitionStartVal');
5189          if( f == 0 ) { f=-50; }   // default value
5190          oFrom = { opacity: 0, translateY: f };
5191          oTo =   { opacity: 1, translateY: 0 };
5192          break;
5193        case 'FLIPUP':
5194          var f=G.tn.opt.Get('displayTransitionStartVal');
5195          if( f == 0 ) { f=100; }   // default value
5196          oFrom = { opacity: 0, translateY: f, rotateX: 45 };
5197          oTo =   { opacity: 1, translateY: 0, rotateX: 0  };
5198          break;
5199          
5200        case 'FLIPDOWN':
5201          var f=G.tn.opt.Get('displayTransitionStartVal');
5202          if( f == 0 ) { f=-100; }   // default value
5203          oFrom = { opacity: 0, translateY: f, rotateX: -45 };
5204          oTo =   { opacity: 1, translateY: 0, rotateX: 0 };
5205          break;
5206        case 'SLIDEUP2':
5207          var f=G.tn.opt.Get('displayTransitionStartVal');
5208          if( f == 0 ) { f=100; }   // default value
5209          oFrom = { opacity: 0, translateY: f, rotateY: 40 };
5210          oTo =   { opacity: 1, translateY: 0, rotateY: 0  };
5211          break;
5212        case 'SLIDEDOWN2':
5213          var f=G.tn.opt.Get('displayTransitionStartVal');
5214          if( f == 0 ) { f=-100; }   // default value
5215          oFrom = { opacity: 0, translateY: f, rotateY: 40 };
5216          oTo =   { opacity: 1, translateY: 0, rotateY: 0  };
5217          break;
5218        case 'SLIDERIGHT':
5219          var f=G.tn.opt.Get('displayTransitionStartVal');
5220          if( f == 0 ) { f=-150; }   // default value
5221          oFrom = { opacity: 0, translateX: f };
5222          oTo =   { opacity: 1, translateX: 0 };
5223          break;
5224
5225        case 'SLIDELEFT':
5226          var f=G.tn.opt.Get('displayTransitionStartVal');
5227          if( f == 0 ) { f=150; }   // default value
5228          oFrom = { opacity: 0, translateX: f };
5229          oTo =   { opacity: 1, translateX: 0 };
5230          break;
5231          
5232        case 'FADEIN':
5233          oFrom = { opacity: 0 };
5234          oTo =   { opacity: 1 };
5235          break;
5236          
5237          
5238      }
5239    
5240      var tweenable = new NGTweenable();
5241      tweenable.tween({
5242        from:         oFrom,
5243        to:           oTo,
5244        attachment:   { $e:item.$elt, item: item, tw: tweenable },
5245        delay:        delay,
5246        duration:     G.tn.opt.Get('displayTransitionDuration'),
5247        easing:       G.tn.opt.Get('displayTransitionEasing'),
5248        step:         function (state, att) {
5249          window.requestAnimationFrame( function() {
5250            if( att.item.$elt === null ) {  // the thumbnail may have been destroyed since the start of the animation
5251              att.tw.stop(false);
5252              return;
5253            }
5254            switch (G.tn.opt.Get('displayTransition')) {
5255              case 'RANDOMSCALE':
5256                att.$e.css( G.CSStransformName , 'scale(' + state.scale + ')').css('opacity', state.opacity);
5257                break;
5258              case 'SCALEUP':
5259                att.$e.css( G.CSStransformName , 'scale('+state.scale+')').css('opacity',state.opacity);
5260                break;
5261              case 'SCALEDOWN':
5262                att.item.$elt.last().css('opacity', state.opacity);
5263                att.item.CSSTransformSet('.nGY2GThumbnail', 'scale', state.scale);
5264                att.item.CSSTransformApply('.nGY2GThumbnail');
5265                break;
5266              case 'SLIDEUP':
5267                att.item.$elt.css('opacity', state.opacity);
5268                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', '0px, '+state.translateY + 'px');
5269                att.item.CSSTransformApply('.nGY2GThumbnail');
5270                break;
5271              case 'SLIDEDOWN':
5272                att.item.$elt.css('opacity', state.opacity);
5273                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', '0px,'+state.translateY+'px');
5274                att.item.CSSTransformApply('.nGY2GThumbnail');
5275                break;
5276              case 'FLIPUP':
5277                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', '0px,'+state.translateY+'px');
5278                att.item.CSSTransformSet('.nGY2GThumbnail', 'rotateX', state.rotateX+'deg');
5279                att.item.$elt.css('opacity', state.opacity);
5280                att.item.CSSTransformApply('.nGY2GThumbnail');
5281                break;
5282              case 'FLIPDOWN':
5283                att.item.$elt.css('opacity', state.opacity);
5284                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', '0px,' + state.translateY + 'px');
5285                att.item.CSSTransformSet('.nGY2GThumbnail', 'rotateX', state.rotateX + 'deg');
5286                att.item.CSSTransformApply('.nGY2GThumbnail');
5287                break;
5288              case 'SLIDEUP2':
5289                att.item.$elt.css('opacity', state.opacity);
5290                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', '0px,' + state.translateY + 'px');
5291                att.item.CSSTransformSet('.nGY2GThumbnail', 'rotateY', state.rotateY + 'deg');
5292                att.item.CSSTransformApply('.nGY2GThumbnail');
5293                break;
5294              case 'SLIDEDOWN2':
5295                att.item.$elt.css('opacity', state.opacity);
5296                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', '0px, ' + state.translateY + 'px');
5297                att.item.CSSTransformSet('.nGY2GThumbnail', 'rotateY', state.rotateY + 'deg');
5298                att.item.CSSTransformApply('.nGY2GThumbnail');
5299                break;
5300              case 'SLIDERIGHT':
5301                att.item.$elt.css('opacity', state.opacity);
5302                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', state.translateX + 'px, 0px');
5303                att.item.CSSTransformApply('.nGY2GThumbnail');
5304                break;
5305              case 'SLIDELEFT':
5306                att.item.CSSTransformSet('.nGY2GThumbnail', 'translate', state.translateX + 'px, 0px');
5307                att.item.$elt.css('opacity', state.opacity);
5308                att.item.CSSTransformApply('.nGY2GThumbnail');
5309                break;
5310              case 'FADEIN':
5311                att.$e.css(state);
5312                break;
5313            }
5314          });
5315          // att.$e.css( G.CSStransformName , 'scale('+state.scale+')').css('opacity',state.opacity);
5316        },
5317        finish:       function (state, att) {
5318          window.requestAnimationFrame( function() {
5319            if( att.item.$elt === null ) { return; }
5320            
5321            switch (G.tn.opt.Get('displayTransition')) {
5322              case 'RANDOMSCALE':
5323                att.$e.css( G.CSStransformName , 'scale('+state.scale+')').css('opacity', '');
5324                break;
5325              case 'SCALEUP':
5326                att.$e.css( G.CSStransformName , '').css('opacity', '');
5327                break;
5328              case 'SCALEDOWN':
5329                att.item.$elt.last().css('opacity', '');
5330                att.item.CSSTransformSet('.nGY2GThumbnail', 'scale', state.scale);
5331                att.item.CSSTransformApply('.nGY2GThumbnail');
5332                break;
5333              case 'SLIDEUP':
5334                att.item.$elt.css('opacity', '');
5335                break;
5336              case 'SLIDEDOWN':
5337                att.item.$elt.css('opacity', '');
5338                break;
5339              case 'FLIPUP':
5340                att.item.$elt.css('opacity', '');
5341                break;
5342              case 'FLIPDOWN':
5343                att.item.$elt.css('opacity', '');
5344                break;
5345              case 'SLIDEUP2':
5346                att.item.$elt.css('opacity', '');
5347                break;
5348              case 'SLIDEDOWN2':
5349                att.item.$elt.css('opacity', '');
5350                att.item.CSSTransformApply('.nGY2GThumbnail');
5351                break;
5352              case 'SLIDERIGHT':
5353                att.item.$elt.css('opacity', '');
5354                break;
5355              case 'SLIDELEFT':
5356                att.item.$elt.css('opacity', '');
5357                break;
5358              case 'FADEIN':
5359                att.$e.css('opacity', '');
5360                break;
5361            }
5362            ThumbnailAppearFinish(att.item);
5363          });
5364
5365        }
5366      });
5367    
5368    }
5369    
5370    // batch display thumbnails with animation
5371    function ThumbnailDisplayAnimBatch() {
5372      
5373      G.GOM.thumbnails2Display.forEach( function(one) {
5374        ThumbnailDisplayAnim2(one.itm, one.d);
5375      });
5376      G.GOM.thumbnails2Display=[];
5377    }
5378    
5379    
5380
5381    // ######################################
5382    // Gallery display animation
5383    function GalleryAppear() {
5384      
5385      var d=G.galleryDisplayTransitionDuration.Get();
5386      switch( G.galleryDisplayTransition.Get() ){
5387        case 'ROTATEX':
5388          G.$E.base.css({ perspective: '1000px', 'perspective-origin': '50% 0%' });
5389          var tweenable = new NGTweenable();
5390          tweenable.tween({
5391            from:         { r: 50 },
5392            to:           { r: 0  },
5393            attachment:   { orgIdx: G.GOM.albumIdx },
5394            duration:     d,
5395            easing:       'easeOutCirc',
5396            step:         function (state, att) {
5397              if( att.orgIdx == G.GOM.albumIdx ) {
5398                // window.ng_draf( function() {
5399                  G.$E.conTnParent.css( G.CSStransformName , 'rotateX(' + state.r + 'deg)');
5400                // });
5401              }
5402            }
5403          });
5404          break;
5405        case 'SLIDEUP':
5406          G.$E.conTnParent.css({ opacity: 0 });
5407          var tweenable = new NGTweenable();
5408          tweenable.tween({
5409            from:         { y: 200, o: 0 },
5410            to:           { y: 0,   o: 1 },
5411            attachment:   { orgIdx: G.GOM.albumIdx },
5412            duration:     d,
5413            easing:       'easeOutCirc',
5414            step:         function (state, att) {
5415              if( att.orgIdx == G.GOM.albumIdx ) {
5416                // window.ng_draf( function() {
5417                  G.$E.conTnParent.css( G.CSStransformName , 'translate( 0px, '+state.y + 'px)').css('opacity', state.o);
5418                // });
5419              }
5420            }
5421          });
5422          break;
5423        case 'NONE':
5424        default:
5425          break;
5426      }
5427
5428
5429    }
5430    
5431    // ######################################
5432    // ##### THUMBNAIL HOVER MANAGEMENT #####
5433    // ######################################
5434
5435    function ThumbnailOverInit( GOMidx ) {
5436      // Over init in 2 step:
5437      // 1) init with thumbnailBuildInit2 parameter
5438      // 2) init with the hover effect parameter
5439      
5440      
5441      var curTn = G.GOM.items[GOMidx];
5442      var item = G.I[curTn.thumbnailIdx];
5443
5444      if( item.$elt == null ) { return; } // zombie
5445      
5446      var fu = G.O.fnThumbnailHoverInit;
5447      if( fu !== null ) {
5448        typeof fu == 'function' ? fu($e, item, GOMidx) : window[fu]($e, item, GOMidx);
5449      }
5450
5451      // build initialization
5452      var inits = G.tn.buildInit.get();
5453      for( var j = 0; j < inits.length; j++) {
5454        switch( inits[j].property ) {
5455          // CSS Transform
5456          case 'scale':
5457          case 'rotateX':
5458          case 'rotateY':
5459          case 'rotateZ':
5460          case 'translateX':
5461          case 'translateY':
5462          case 'translateZ':
5463            item.CSSTransformSet(inits[j].element, inits[j].property, inits[j].value);
5464            item.CSSTransformApply(inits[j].element);
5465            break;
5466          // CSS filter
5467          case 'blur':
5468          case 'brightness':
5469          case 'grayscale':
5470          case 'sepia':
5471          case 'contrast':
5472          case 'opacity':
5473          case 'saturate':
5474            item.CSSFilterSet(inits[j].element, inits[j].property, inits[j].value);
5475            item.CSSFilterApply(inits[j].element);
5476            break;
5477          default:
5478            var $t=item.$getElt(inits[j].element);
5479            $t.css( inits[j].property, inits[j].value );
5480            break;
5481        }
5482      }
5483      
5484      // hover
5485      var effects = G.tn.hoverEffects.get();
5486      for( var j = 0; j < effects.length; j++) {
5487        if( effects[j].firstKeyframe === true ) {
5488          switch( effects[j].type ) {
5489            case 'scale':
5490            case 'rotateX':
5491            case 'rotateY':
5492            case 'rotateZ':
5493            case 'translateX':
5494            case 'translateY':
5495            case 'translateZ':
5496              item.CSSTransformSet(effects[j].element, effects[j].type, effects[j].from);
5497              item.CSSTransformApply(effects[j].element);
5498              break;
5499            case 'blur':
5500            case 'brightness':
5501            case 'grayscale':
5502            case 'sepia':
5503            case 'contrast':
5504            case 'opacity':
5505            case 'saturate':
5506              item.CSSFilterSet(effects[j].element, effects[j].type, effects[j].from);
5507              item.CSSFilterApply(effects[j].element);
5508              break;
5509            default:
5510              var $t = item.$getElt(effects[j].element);
5511              $t.css( effects[j].type, effects[j].from );
5512              break;
5513              
5514          }
5515        }
5516      }
5517      item.hoverInitDone=true;
5518    }
5519
5520    function ThumbnailHoverReInitAll() {
5521      if( G.GOM.albumIdx == -1 ) { return; };
5522      var l = G.GOM.items.length;
5523      for( var i = 0; i < l ; i++ ) {
5524        ThumbnailOverInit(i);
5525        // G.GOM.items[i].hovered=false;
5526        G.I[G.GOM.items[i].thumbnailIdx].hovered = false;
5527      }
5528    }
5529
5530
5531    function ThumbnailHover( GOMidx ) {
5532      if( G.GOM.albumIdx == -1 || !G.galleryResizeEventEnabled ) { return; };
5533      if( G.GOM.slider.hostIdx == GOMidx ) {
5534        // slider hosted on thumbnail -> no hover effect
5535        return;
5536      }
5537      var curTn = G.GOM.items[GOMidx];
5538      var item = G.I[curTn.thumbnailIdx];
5539      if( item.kind == 'albumUp' || item.$elt == null ) { return; }
5540
5541      item.hovered = true;
5542
5543      var fu = G.O.fnThumbnailHover;
5544      if( fu !== null ) {
5545        typeof fu == 'function' ? fu(item.$elt, item, GOMidx) : window[fu](item.$elt, item, GOMidx);
5546      }
5547      var effects = G.tn.hoverEffects.get();
5548
5549      try {
5550        for( var j = 0; j < effects.length; j++) {
5551          if( effects[j].hoverin === true ) {
5552            //item.animate( effects[j], j*10,  true );
5553            item.animate( effects[j], 0,  true );
5554          }
5555        }
5556        // effects on whole layout
5557        // GalleryResize( GOMidx );
5558      }
5559      catch (e) { 
5560        NanoAlert(G, 'error on hover: ' + e.message );
5561      }
5562
5563    }
5564
5565    function ThumbnailHoverOutAll() {
5566      if( G.GOM.albumIdx == -1 ) { return; };
5567      var l = G.GOM.items.length;
5568      for( var i = 0; i < l ; i++ ) {
5569        if( G.GOM.items[i].inDisplayArea ) {
5570          ThumbnailHoverOut(i);
5571        }
5572        else {
5573          G.I[G.GOM.items[i].thumbnailIdx].hovered = false;
5574        }
5575      }
5576    }
5577
5578    
5579    function ThumbnailHoverOut( GOMidx ) {
5580      if( G.GOM.albumIdx == -1 || !G.galleryResizeEventEnabled ) { return; }
5581
5582      if( G.GOM.slider.hostIdx == GOMidx ) {
5583        // slider on thumbnail -> no hover effect
5584        return;
5585      }
5586
5587      var curTn = G.GOM.items[GOMidx];
5588      var item = G.I[curTn.thumbnailIdx];
5589      if( item.kind == 'albumUp' || !item.hovered ) { return; }
5590      item.hovered = false;
5591      if( item.$elt == null ) { return; }
5592
5593      var fu = G.O.fnThumbnailHoverOut;
5594      if( fu !== null ) {
5595        typeof fu == 'function' ? fu(item.$elt, item, GOMidx) : window[fu](item.$elt, item, GOMidx);
5596      }
5597
5598      var effects = G.tn.hoverEffects.get();
5599      try {
5600        for( var j = 0; j < effects.length; j++) {
5601          if( effects[j].hoverout === true ) {
5602            // item.animate( effects[j], j*10, false );
5603            item.animate( effects[j], 0, false );
5604          }
5605        }
5606        // effects on whole layout
5607        // GalleryResize( );
5608      }
5609      catch (e) { 
5610        NanoAlert(G, 'error on hoverOut: ' + e.message );
5611      }
5612      
5613    }
5614    
5615
5616    /** @function DisplayPhoto */
5617    function DisplayPhoto( imageID, albumID ) {
5618
5619      if( G.O.debugMode ) { console.log('#DisplayPhoto : '+  albumID +'-'+ imageID); }
5620      var albumIdx = NGY2Item.GetIdx(G, albumID);
5621      if( albumIdx == 0 ) {
5622        G.GOM.curNavLevel = 'l1';
5623      }
5624      else {
5625        G.GOM.curNavLevel = 'lN';
5626      }
5627
5628      if( albumIdx == -1 ) {
5629        // get content of album on root level
5630        if( G.O.kind != '' ) {
5631          // do not add album if Markup or Javascript data
5632          NGY2Item.New( G, '', '', albumID, '0', 'album' );    // create empty album
5633          albumIdx = G.I.length - 1;
5634        }
5635      }
5636
5637      var ngy2ItemIdx = NGY2Item.GetIdx(G, imageID);
5638      if( ngy2ItemIdx == -1 ) {
5639        // get content of the album
5640        AlbumGetContent( albumID, DisplayPhoto, imageID, albumID );
5641        return;
5642      }
5643      
5644      if( G.O.debugMode ) { console.log('#DisplayPhoto : '+  ngy2ItemIdx); }
5645     
5646      DisplayPhotoIdx(ngy2ItemIdx);
5647    
5648    }
5649
5650
5651    // BETA -> NOT finished and not used at this time
5652    // Retrieve the title+description of ONE album
5653    function albumGetInfo( albumIdx, fnToCall ) {
5654      var url =   '';
5655      var kind =  'image';
5656      
5657      switch( G.O.kind ) {
5658        case 'json':
5659          // TODO
5660        case 'flickr':
5661          // TODO
5662        case 'picasa':
5663        case 'google':
5664        case 'google2':
5665        default:
5666          url = G.Google.url() + 'user/'+G.O.userID+'/albumid/'+G.I[albumIdx].GetID()+'?alt=json&&max-results=1&fields=title';
5667          break;
5668      }
5669
5670      jQuery.ajaxSetup({ cache: false });
5671      jQuery.support.cors = true;
5672      
5673      var tId = setTimeout( function() {
5674        // workaround to handle JSONP (cross-domain) errors
5675        //PreloaderHide();
5676        NanoAlert(G, 'Could not retrieve AJAX data...');
5677      }, 60000 );
5678      jQuery.getJSON(url, function(data, status, xhr) {
5679        clearTimeout(tId);
5680        //PreloaderHide();
5681        
5682        fnToCall( G.I[albumIdx].GetID() );
5683
5684      })
5685      .fail( function(jqxhr, textStatus, error) {
5686        clearTimeout(tId);
5687        //PreloaderHide();
5688        var err = textStatus + ', ' + error;
5689        NanoAlert('Could not retrieve ajax data: ' + err);
5690      });      
5691    
5692    }
5693
5694    
5695    // function AlbumGetContent( albumIdx, fnToCall ) {
5696    function AlbumGetContent( albumID, fnToCall, fnParam1, fnParam2 ) {
5697      // var url='';
5698      // var kind='image';
5699      // var albumIdx=NGY2Item.GetIdx(G, albumID);
5700      // var photoIdx=NGY2Item.GetIdx(G, photoID);
5701      
5702      switch( G.O.kind ) {
5703        // MARKUP / API
5704        case '':
5705          AlbumGetMarkupOrApi(fnToCall, fnParam1, fnParam2);
5706          break;
5707        // JSON, Flickr, Picasa, ...
5708        default:
5709          jQuery.nanogallery2['data_'+G.O.kind](G, 'AlbumGetContent', albumID, fnToCall, fnParam1, fnParam2 );
5710      }
5711      
5712    }
5713    
5714    var mediaList = {
5715      youtube : {
5716        getID: function( url ) {
5717          // https://stackoverflow.com/questions/10591547/how-to-get-youtube-video-id-from-url
5718          var s = url.match( /(?:https?:\/{2})?(?:w{3}\.)?youtu(?:be)?\.(?:com|be)(?:\/watch\?v=|\/)([^\s&]+)/ );
5719          return s != null ? s[1] : null;
5720        },
5721        thumbUrl: function( id ) {
5722          return 'https://img.youtube.com/vi/' + id + '/hqdefault.jpg';
5723        },
5724        url: function( id ) {
5725          return 'https://www.youtube.com/embed/' + id;
5726        },
5727        markup: function( id ) {
5728          // return '<iframe class="nGY2ViewerMedia" src="https://www.youtube.com/embed/' + id + '?rel=0" frameborder="0" gesture="media" allowfullscreen></iframe>';
5729          return '<iframe class="nGY2ViewerMedia" src="https://www.youtube.com/embed/' + id + '?rel=0" frameborder="0" allow="autoplay" allowfullscreen></iframe>';
5730        },
5731        kind: 'iframe'
5732      },
5733      vimeo : {
5734        getID: function( url ) {
5735          // https://stackoverflow.com/questions/2916544/parsing-a-vimeo-id-using-javascript
5736          var s = url.match( /^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/ );
5737          return s != null ? s[5] : null;
5738        },
5739        url: function( id ) {
5740          return 'https://player.vimeo.com/video/' + id;
5741        },
5742        markup: function( id ) {
5743          // return '<iframe class="nGY2ViewerMedia" src="https://player.vimeo.com/video/' + id + '?rel=0" frameborder="0" gesture="media" allowfullscreen></iframe>';
5744          return '<iframe class="nGY2ViewerMedia" src="https://player.vimeo.com/video/' + id + '?rel=0" frameborder="0" allow="autoplay" allowfullscreen></iframe>';
5745        },
5746        kind: 'iframe'
5747      },
5748      dailymotion : {
5749        getID: function( url ) {
5750          // https://stackoverflow.com/questions/12387389/how-to-parse-dailymotion-video-url-in-javascript
5751          var m = url.match(/^.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/);
5752          if (m !== null) {
5753              if(m[4] !== undefined) {
5754                  return m[4];
5755              }
5756              return m[2];
5757          }
5758          return null;
5759        },
5760        thumbUrl: function( id ) {
5761          return 'https://www.dailymotion.com/thumbnail/video/' + id;
5762        },
5763        url: function( id ) {
5764          return 'https://www.dailymotion.com/embed/video/' + id;
5765        },
5766        markup: function( id ) {
5767          // return '<iframe class="nGY2ViewerMedia" src="https://www.dailymotion.com/embed/video/' + id + '?rel=0" frameborder="0" gesture="media" allowfullscreen></iframe>';
5768          return '<iframe class="nGY2ViewerMedia" src="https://www.dailymotion.com/embed/video/' + id + '?rel=0" frameborder="0" allow="autoplay" allowfullscreen></iframe>';
5769        },
5770        kind: 'iframe'
5771      },
5772      selfhosted : {
5773        // SELF-HOSTED VIDEOS
5774        getID: function( url ) {
5775          // In order to leave things as is, I used ID to identify the extension
5776          // https://stackoverflow.com/questions/6997262/how-to-pull-url-file-extension-out-of-url-string-using-javascript
5777          // Make sure the method used for verifying the extension matches the kind of url your selfhosted video has
5778          var extension = url.split('.').pop();
5779
5780          // supported extensions
5781          var s = ( extension === 'mp4' || extension === 'webm' || extension === 'ogv' || extension === '3gp' ) ? extension : null ;
5782          return s;
5783        },
5784        markup: function( url ) {
5785          // 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>';
5786          var extension = url.split('.').pop();
5787          return '<video controls class="nGY2ViewerMedia"><source src="'+ url +'" type="video/'+ extension +'" preload="auto">Your browser does not support the video tag (HTML 5).</video>';
5788        },
5789        kind: 'video',
5790        selfhosted : true
5791      }
5792    };
5793        
5794    function AlbumGetMarkupOrApi ( fnToCall, fnParam1, fnParam2 ) {
5795    
5796      if( G.markupOrApiProcessed === true ) {
5797        // already processed (maybe location hash to unknow reference) -> display root album
5798        DisplayAlbum('-1', 0);
5799        return;
5800      }
5801      
5802      if( G.O.items !== undefined && G.O.items !== null ) {
5803        // data defined as an object in an option parameter
5804        GetContentApiObject();
5805      }
5806      else {
5807      if( G.O.$markup.length > 0 ) {
5808          // data defined as markup (href elements)
5809          GetContentMarkup( G.O.$markup );
5810          G.O.$markup=[]  ;
5811        }
5812        else {
5813          NanoConsoleLog(G, 'error: no media to process.');
5814          return;
5815        }
5816      }
5817      
5818      G.markupOrApiProcessed = true;
5819      if( fnToCall !== null &&  fnToCall !== undefined) {
5820        fnToCall( fnParam1, fnParam2, null );
5821      }
5822    }
5823    
5824    function StartsWithProtocol ( path ) {
5825      if( path == null || path == undefined ) { return false; }
5826      
5827      var pattern = /^((http|https|ftp|ftps|file):\/\/)/;
5828      if( !pattern.test(path) ) {
5829        // not a full URL
5830        return false;
5831      }
5832      return true;
5833    }
5834    
5835    function GetContentApiObject() {
5836      var foundAlbumID=false;
5837      var nbTitles = 0;
5838      var AlbumPostProcess = NGY2Tools.AlbumPostProcess.bind(G);
5839
5840      G.I[0].contentIsLoaded=true;
5841
5842      jQuery.each(G.O.items, function(i,item){
5843        
5844        var title = '';
5845        title=GetI18nItem(item, 'title');
5846        if( title === undefined ) { title=''; }
5847        
5848        var src='';
5849        if( item['src'+RetrieveCurWidth().toUpperCase()] !== undefined ) {
5850          src = item['src'+RetrieveCurWidth().toUpperCase()];
5851        }
5852        else {
5853          src = item.src;
5854        }
5855        if( !StartsWithProtocol(src) ) {
5856          src = G.O.itemsBaseURL + src;
5857        }
5858
5859        var thumbsrc = '';
5860        if( item.srct !== undefined && item.srct.length > 0 ) {
5861          thumbsrc = item.srct;
5862          if( !StartsWithProtocol(thumbsrc) ) {
5863            thumbsrc = G.O.itemsBaseURL + thumbsrc;
5864          }
5865        }
5866        else {
5867          thumbsrc = src;
5868        }
5869        
5870        var thumbsrcX2 = '';
5871        if( item.srct2x !== undefined && item.srct2x.length > 0 ) {
5872          thumbsrcX2 = item.srct2x;
5873          if( !StartsWithProtocol(thumbsrcX2) ) {
5874            thumbsrcX2 = G.O.itemsBaseURL + thumbsrcX2;
5875          }
5876        }
5877        else {
5878          if( thumbsrc != '' ) {
5879            thumbsrcX2 = thumbsrc;
5880          }
5881          else {
5882            thumbsrcX2 = src;
5883          }
5884        }
5885
5886        if( G.O.thumbnailLabel.get('title') != '' ) {
5887          title = GetImageTitle(src);
5888        }
5889
5890        var description='';     //'&nbsp;';
5891        description=GetI18nItem(item,'description');
5892        if( description === undefined ) { description=''; }
5893        //if( toType(item.description) == 'string' ) {
5894        //  description=item.description;
5895        //}
5896
5897        var tags = GetI18nItem(item, 'tags');
5898        if( tags === undefined ) { tags=''; }
5899
5900        var albumID = 0;
5901        if( item.albumID !== undefined  ) {
5902          albumID=item.albumID;
5903          foundAlbumID = true;
5904        }
5905        var ID = null;
5906        if( item.ID !== undefined ) {
5907          ID = item.ID;
5908        }
5909        var kind = 'image';
5910        if( item.kind !== undefined && item.kind.length > 0 ) {
5911          kind = item.kind;
5912        }
5913        
5914        var newItem=NGY2Item.New( G, title, description, ID, albumID, kind, tags );
5915        if( title != '' ) {
5916          nbTitles++;
5917        }
5918        
5919        // media source url - img is the default media kind
5920        newItem.setMediaURL( src, 'img', description );
5921
5922        // manage media kinds other than IMG
5923        jQuery.each(mediaList, function ( n, media ) {
5924          var id = media.getID(src);
5925          if( id != null ) {
5926            if( typeof media.url == 'function' ) { src = media.url(id);  }
5927            if( typeof media.thumbUrl == 'function' ) { thumbsrc = media.thumbUrl(id);  }
5928            newItem.mediaKind = media.kind;
5929            newItem.mediaMarkup = ( media.selfhosted ) ? media.markup( src ) : media.markup(id);
5930            return false;
5931          }
5932        });
5933
5934        // image size
5935        if( item.imageWidth !== undefined ) { newItem.imageWidth = item.width; }
5936        if( item.imageHeight !== undefined ) { newItem.imageHeight = item.height; }
5937
5938        // THUMBNAILS
5939        
5940        // thumbnail image size
5941        var tw = item.imgtWidth !== undefined ? item.imgtWidth : 0;
5942        var th = item.imgtHeight !== undefined ? item.imgtHeight : 0;
5943
5944        // default thumbnail URL and size
5945        newItem.thumbs = {
5946          url:    { l1 : { xs: thumbsrc, sm: thumbsrc, me: thumbsrc, la: thumbsrc, xl: thumbsrc }, lN : { xs: thumbsrc, sm: thumbsrc, me: thumbsrc, la: thumbsrc, xl: thumbsrc } },
5947          width:  { l1 : { xs: tw, sm: tw, me: tw, la: tw, xl: tw }, lN : { xs: tw, sm: tw, me: tw, la: tw, xl: tw } },
5948          height: { l1 : { xs: th, sm: th, me: th, la: th, xl: th }, lN : { xs: th, sm: th, me: th, la: th, xl: th } }
5949        };
5950        
5951        // default media type -> IMG
5952        if( newItem.mediaKind == 'img' ) {
5953        
5954          // responsive thumbnails URL and size
5955          var lst=['xs', 'sm', 'me', 'la', 'xl'];
5956          for( var i=0; i< lst.length; i++ ) {
5957            // url
5958            var turl = item['srct' + lst[i].toUpperCase()];
5959            if( turl !== undefined ) {
5960              if( !StartsWithProtocol(turl) ) {
5961                turl = G.O.itemsBaseURL + turl;
5962              }
5963              newItem.url.l1[lst[i]] = turl;
5964              newItem.url.lN[lst[i]] = turl;
5965            }
5966            // width
5967            var tw = item['imgt' + lst[i].toUpperCase() + 'Width'];
5968            if( tw != undefined ) {
5969              newItem.width.l1[lst[i]] = parseInt(tw);
5970              newItem.width.lN[lst[i]] = parseInt(tw);
5971            }
5972            // height
5973            var th = item['imgt' + lst[i].toUpperCase() + 'Height'];
5974            if( th != undefined ) {
5975              newItem.height.l1[lst[i]] = parseInt(th);
5976              newItem.height.lN[lst[i]] = parseInt(th);
5977            }
5978          }
5979        }
5980        
5981        // dominant colors (needs to be a base64 gif)
5982        if( item.imageDominantColors !== undefined ) {
5983          newItem.imageDominantColors = item.imageDominantColors;
5984        }
5985        // dominant color (rgb hex)
5986        if( item.imageDominantColor !== undefined ) {
5987          newItem.imageDominantColor = item.imageDominantColor;
5988        }
5989        
5990        // dest url
5991        if( item.destURL !== undefined && item.destURL.length>0 ) {
5992          newItem.destinationURL = item.destURL;
5993        }
5994        
5995        // download image url
5996        if( item.downloadURL !== undefined && item.downloadURL.length>0 ) {
5997          newItem.downloadURL = item.downloadURL;
5998        }
5999        
6000        // EXIF DATA
6001        // Exif - model
6002        if( item.exifModel !== undefined ) { newItem.exif.model = item.exifModel; }
6003        // Exif - flash
6004        if( item.exifFlash !== undefined ) { newItem.exif.flash = item.exifFlash; }
6005        // Exif - focallength
6006        if( item.exifFocalLength !== undefined ) { newItem.exif.focallength = item.exifFocalLength; }
6007        // Exif - fstop
6008        if( item.exifFStop !== undefined ) { newItem.exif.fstop = item.exifFStop; }
6009        // Exif - exposure
6010        if( item.exifExposure !== undefined ) { newItem.exif.exposure = item.exifExposure; }
6011        // Exif - time
6012        if( item.exifIso !== undefined ) { newItem.exif.iso = item.exifIso; }
6013        // Exif - iso
6014        if( item.exifTime !== undefined ) { newItem.exif.time = item.exifTime; }
6015        // Exif - location
6016        if( item.exifLocation !== undefined ) { newItem.exif.location  = item.exifLocation; }
6017        
6018        
6019        // custom data
6020        if( item.customData !== null ) {
6021          newItem.customData = cloneJSObject( item.customData );
6022        }
6023
6024        newItem.contentIsLoaded = true;
6025        
6026        var fu = G.O.fnProcessData;
6027        if( fu !== null ) {
6028          typeof fu == 'function' ? fu(newItem, 'api', item) : window[fu](newItem, 'api', item);
6029        }
6030        
6031        AlbumPostProcess(albumID);
6032      });
6033      
6034      // if( foundAlbumID ) { G.O.displayBreadcrumb=true; }
6035      if( nbTitles == 0 ) { G.O.thumbnailLabel.display=false; }
6036
6037    }
6038    
6039    
6040    function GetContentMarkup( $elements ) {
6041      var foundAlbumID = false;
6042      var nbTitles = 0;
6043      var AlbumPostProcess = NGY2Tools.AlbumPostProcess.bind(G);
6044
6045      G.I[0].contentIsLoaded = true;
6046
6047      jQuery.each($elements, function(i, item){
6048
6049        // create dictionnary with all data attribute name in lowercase (to be case unsensitive)
6050        var data = {
6051          // some default values
6052          'data-ngdesc':                  '',         // item description
6053          'data-ngid':                    null,       // ID
6054          'data-ngkind':                  'image',    // kind (image, album, albumup)
6055          'data-ngtags':                  null,       // tags
6056          'data-ngdest':                  '',         // destination URL
6057          'data-ngthumbimgwidth':         0,          // thumbnail width
6058          'data-ngthumbimgheight':        0,          // thumbnail height
6059          'data-ngimagewidth':            0,          // image width
6060          'data-ngimageheight':           0,          // image height
6061          'data-ngimagedominantcolors':   null,       // image dominant colors
6062          'data-ngimagedominantcolor':    null,       // image dominant colors
6063          'data-ngexifmodel':             '',         // EXIF data
6064          'data-ngexifflash':             '',
6065          'data-ngexiffocallength':       '',
6066          'data-ngexiffstop':             '',
6067          'data-ngexifexposure':          '',
6068          'data-ngexifiso':               '',
6069          'data-ngexiftime':              '',
6070          'data-ngexiflocation':          ''
6071        };
6072        [].forEach.call( item.attributes, function(attr) {
6073          data[attr.name.toLowerCase()] = attr.value;
6074        });
6075
6076        // responsive image source
6077        var src = '',
6078        st = RetrieveCurWidth().toUpperCase();
6079        if( data.hasOwnProperty('data-ngsrc'+st) ) {
6080          src = data['data-ngsrc'+st];
6081        }
6082        if( src == '' ) {
6083          src = data['href'];
6084        }
6085        if( !StartsWithProtocol(src) ) {
6086          src = G.O.itemsBaseURL + src;
6087        }
6088
6089        // thumbnail
6090        var thumbsrc = '';
6091        if( data.hasOwnProperty('data-ngthumb') ) {
6092          thumbsrc = data['data-ngthumb'];
6093          if( !StartsWithProtocol(thumbsrc) ) {
6094            thumbsrc = G.O.itemsBaseURL + thumbsrc;
6095          }
6096        }
6097        else {
6098          thumbsrc = src;
6099        }
6100        var thumbsrcX2 = '';
6101        if( data.hasOwnProperty('data-ngthumb2x') ) {
6102          thumbsrcX2 = data['data-ngthumb2x'];
6103          if( !StartsWithProtocol(thumbsrcX2) ) {
6104            thumbsrcX2 = G.O.itemsBaseURL + thumbsrcX2;
6105          }
6106        }
6107        
6108        //newObj.description=jQuery(item).attr('data-ngdesc');
6109        var description = data['data-ngdesc'];
6110        var ID = data['id'];
6111        if( ID == undefined ) {
6112          ID = data['data-ngid'];
6113        }
6114        var kind = data['data-ngkind'];
6115        var tags = data['data-ngtags'];
6116
6117        var albumID = '0';
6118        if( data.hasOwnProperty('data-ngalbumid') ) {
6119          albumID = data['data-ngalbumid'];
6120          foundAlbumID = true;
6121        }
6122        
6123        var title = jQuery(item).text();
6124        if( !(G.O.thumbnailLabel.get('title') == '' || G.O.thumbnailLabel.get('title') == undefined) ) {
6125          title = GetImageTitle(src);
6126        }
6127
6128
6129        var newItem = NGY2Item.New( G, title, description, ID, albumID, kind, tags );
6130        if( title != '' ) {
6131          nbTitles++;
6132        }
6133
6134        // media source url - img is the default media kind
6135        newItem.setMediaURL( src, 'img', description);
6136
6137        // manage media kinds other than IMG
6138        newItem.mediaKind = 'img';
6139        jQuery.each(mediaList, function ( n, media ) {
6140          var id = media.getID(src);
6141          if( id != null ) {
6142            if( typeof media.url == 'function' ) { src = media.url(id);  }
6143            if( typeof media.thumbUrl == 'function' ) { thumbsrc = media.thumbUrl(id);  }
6144            newItem.mediaKind = media.kind;
6145            newItem.mediaMarkup = ( media.selfhosted ) ? media.markup( src ) : media.markup(id);
6146            return false;
6147          }
6148        });
6149        
6150        
6151        // image size
6152        newItem.imageWidth = parseInt( data['data-ngimagewidth'] );
6153        newItem.imageHeight = parseInt( data['data-ngimageheight'] );
6154        
6155        // default thumbnail image URL and size
6156        var tw = parseInt(data['data-ngthumbimgwidth']);
6157        var th = parseInt(data['data-ngthumbimgheight']);
6158        newItem.thumbs = {
6159          url:    { l1 : { xs: thumbsrc, sm: thumbsrc, me: thumbsrc, la: thumbsrc, xl: thumbsrc }, lN : { xs: thumbsrc, sm: thumbsrc, me: thumbsrc, la: thumbsrc, xl: thumbsrc } },
6160          width:  { l1 : { xs: tw, sm: tw, me: tw, la: tw, xl: tw }, lN : { xs: tw, sm: tw, me: tw, la: tw, xl: tw } },
6161          height: { l1 : { xs: th, sm: th, me: th, la: th, xl: th }, lN : { xs: th, sm: th, me: th, la: th, xl: th } }
6162        };
6163
6164        // default media type -> IMG
6165        if( newItem.mediaKind == 'img' ) {
6166        
6167          // responsive thumbnails URL and size
6168          var lst = ['xs', 'sm', 'me', 'la', 'xl'];
6169          for( var i = 0; i < lst.length; i++ ) {
6170            // url
6171            if( data.hasOwnProperty('data-ngthumb' + lst[i]) ) {
6172              var turl=data['data-ngthumb' + lst[i]];
6173              if( !StartsWithProtocol(turl) ) {
6174                turl = G.O.itemsBaseURL + turl;
6175              }
6176              newItem.url.l1[lst[i]] = turl;
6177              newItem.url.lN[lst[i]] = turl;
6178            }
6179
6180            // width
6181            if( data.hasOwnProperty('data-ngthumb' + lst[i] + 'width') ) {
6182              var tw=parseInt(data['data-ngthumb' + lst[i] + 'width']);
6183              newItem.width.l1[lst[i]] = tw;
6184              newItem.width.lN[lst[i]] = tw;
6185            }
6186            // height
6187            if( data.hasOwnProperty('data-ngthumb' + lst[i] + 'height') ) {
6188              var th=parseInt('data-ngthumb' + lst[i] + 'height');
6189              newItem.height.l1[lst[i]] = th;
6190              newItem.height.lN[lst[i]] = th;
6191            }
6192          }
6193        }
6194        
6195        
6196        // dominant colorS (needs to be a base64 gif)
6197        newItem.imageDominantColors = data['data-ngimagedominantcolors'];
6198        // dominant color (rgb hex)
6199        newItem.imageDominantColor = data['data-ngimagedominantcolors'];
6200
6201        newItem.destinationURL = data['data-ngdest'];
6202        newItem.downloadURL = data['data-ngdownloadurl'];
6203
6204        // Exif - model
6205        newItem.exif.model=data['data-ngexifmodel'];
6206        // Exif - flash
6207        newItem.exif.flash=data['data-ngexifflash'];
6208        // Exif - focallength
6209        newItem.exif.focallength=data['data-ngexiffocallength'];
6210        // Exif - fstop
6211        newItem.exif.fstop=data['data-ngexiffstop'];
6212        // Exif - exposure
6213        newItem.exif.exposure=data['data-ngexifexposure'];
6214        // Exif - iso
6215        newItem.exif.iso=data['data-ngexifiso'];
6216        // Exif - time
6217        newItem.exif.time=data['data-ngexiftime'];
6218        // Exif - location
6219        newItem.exif.location=data['data-ngexiflocation'];
6220        
6221        newItem.contentIsLoaded=true;
6222
6223        // custom data
6224        if( jQuery(item).data('customdata') !== undefined ) {
6225          newItem.customData=cloneJSObject(jQuery(item).data('customdata'));
6226        }
6227        // custom data
6228        if( jQuery(item).data('ngcustomdata') !== undefined ) {
6229          newItem.customData=cloneJSObject(jQuery(item).data('ngcustomdata'));
6230        }
6231
6232        var fu=G.O.fnProcessData;
6233        if( fu !== null ) {
6234          typeof fu == 'function' ? fu(newItem, 'markup', item) : window[fu](newItem, 'markup', item);
6235        }
6236        
6237        AlbumPostProcess(albumID);
6238
6239      });
6240      
6241      // if( foundAlbumID ) { G.O.displayBreadcrumb=true; }
6242      if( nbTitles == 0 ) { G.O.thumbnailLabel.display = false; }
6243
6244    }
6245
6246    
6247    // ################################
6248    // ##### DEFINE VARIABLES     #####
6249    // ################################
6250
6251    
6252    /** @function DefineVariables */
6253    function DefineVariables() {
6254    
6255      // change 'picasa' to 'google' for compatibility reason
6256      if( G.O.kind.toUpperCase() == 'PICASA'  || G.O.kind.toUpperCase() == 'GOOGLE') {
6257        G.O.kind='google2';
6258      }
6259    
6260      // management of screen width
6261      G.GOM.cache.viewport = getViewport();
6262      G.GOM.curWidth = RetrieveCurWidth();
6263      
6264      // tumbnail toolbar
6265      jQuery.extend(true, G.tn.toolbar.image, G.O.thumbnailToolbarImage );
6266      jQuery.extend(true, G.tn.toolbar.album, G.O.thumbnailToolbarAlbum );
6267      var t = ['image', 'album'];
6268      var pos= ['topLeft', 'topRight', 'bottomLeft', 'bottomRight']
6269      for( var i=0; i < t.length ; i++ ) {
6270        for( var j=0; j < pos.length ; j++ ) {
6271          G.tn.toolbar[t[i]][pos[j]] = G.tn.toolbar[t[i]][pos[j]].toUpperCase();
6272        }
6273      }
6274
6275      // thumbnails label - level dependant settings
6276      G.O.thumbnailLabel.get = function( opt ) {
6277        if( G.GOM.curNavLevel == 'l1' && G.O.thumbnailL1Label !== undefined && G.O.thumbnailL1Label[opt] !== undefined ) {
6278          return G.O.thumbnailL1Label[opt];
6279        }
6280        else {
6281          return G.O.thumbnailLabel[opt];
6282        }
6283      };
6284      G.O.thumbnailLabel.set = function( opt, value ) {
6285        if( G.GOM.curNavLevel == 'l1' && G.O.thumbnailL1Label !== undefined && G.O.thumbnailL1Label[opt] !== undefined ) {
6286          G.O.thumbnailL1Label[opt]=value;
6287        }
6288        else {
6289          G.O.thumbnailLabel[opt]=value;
6290        }
6291      };
6292
6293      if( G.O.blackList != '' ) { G.blackList=G.O.blackList.toUpperCase().split('|'); }
6294      if( G.O.whiteList != '' ) { G.whiteList=G.O.whiteList.toUpperCase().split('|'); }
6295
6296      if( G.O.albumList2 !== undefined && G.O.albumList2 !== null && G.O.albumList2.constructor === Array  ) {
6297        var l=G.O.albumList2.length;
6298        for(var i=0; i< l; i++ ) {
6299          G.albumList.push(G.O.albumList2[i]);
6300        }
6301        // G.albumList=G.O.albumList.toUpperCase().split('|');
6302      }
6303      if( G.O.albumList2 !== undefined && typeof G.O.albumList2 == 'string'   ) {
6304        G.albumList.push(G.O.albumList2);
6305      }
6306      
6307      
6308      // thumbnail image crop
6309      G.tn.opt.lN.crop = G.O.thumbnailCrop;
6310      G.tn.opt.l1.crop = G.O.thumbnailL1Crop != null ? G.O.thumbnailL1Crop : G.O.thumbnailCrop;
6311
6312
6313      function ThumbnailOpt( lN, l1, opt) {
6314        G.tn.opt.lN[opt]=G.O[lN];
6315        G.tn.opt.l1[opt]=G.O[lN];
6316        if( toType(G.O[l1]) == 'number' ) {
6317          G.tn.opt.l1[opt]=G.O[l1];
6318        }
6319      }
6320      // thumbnail stacks
6321      ThumbnailOpt('thumbnailStacks', 'thumbnailL1Stacks', 'stacks');
6322      // thumbnail stacks translate X
6323      ThumbnailOpt('thumbnailStacksTranslateX', 'thumbnailL1StacksTranslateX', 'stacksTranslateX');
6324      // thumbnail stacks translate Y
6325      ThumbnailOpt('thumbnailStacksTranslateY', 'thumbnailL1StacksTranslateY', 'stacksTranslateY');
6326      // thumbnail stacks translate Z
6327      ThumbnailOpt('thumbnailStacksTranslateZ', 'thumbnailL1StacksTranslateZ', 'stacksTranslateZ');
6328      // thumbnail stacks rotate X
6329      ThumbnailOpt('thumbnailStacksRotateX', 'thumbnailL1StacksRotateX', 'stacksRotateX');
6330      // thumbnail stacks rotate Y
6331      ThumbnailOpt('thumbnailStacksRotateY', 'thumbnailL1StacksRotateY', 'stacksRotateY');
6332      // thumbnail stacks rotate Z
6333      ThumbnailOpt('thumbnailStacksRotateZ', 'thumbnailL1StacksRotateZ', 'stacksRotateZ');
6334      // thumbnail stacks scale
6335      ThumbnailOpt('thumbnailStacksScale', 'thumbnailL1StacksScale', 'stacksScale');
6336      // thumbnail gutter width
6337      ThumbnailOpt('thumbnailGutterWidth', 'thumbnailL1GutterWidth', 'gutterWidth');
6338      // thumbnail gutter height
6339      ThumbnailOpt('thumbnailGutterHeight', 'thumbnailL1GutterHeight', 'gutterHeight');
6340      // thumbnail grid base height (for cascading layout)
6341      ThumbnailOpt('thumbnailBaseGridHeight', 'thumbnailL1BaseGridHeight', 'baseGridHeight');
6342      
6343      // gallery display mode
6344      G.galleryDisplayMode.lN = G.O.galleryDisplayMode.toUpperCase();
6345      G.galleryDisplayMode.l1 = G.O.galleryL1DisplayMode != null ? G.O.galleryL1DisplayMode.toUpperCase() : G.O.galleryDisplayMode.toUpperCase();
6346      
6347      // gallery maximum number of lines of thumbnails
6348      G.galleryMaxRows.lN = G.O.galleryMaxRows;
6349      G.galleryMaxRows.l1 = toType(G.O.galleryL1MaxRows) == 'number' ? G.O.galleryL1MaxRows : G.O.galleryMaxRows;
6350
6351      // gallery last row full
6352      G.galleryLastRowFull.lN = G.O.galleryLastRowFull;
6353      G.galleryLastRowFull.l1 = G.O.galleryL1LastRowFull != null ? G.O.galleryL1LastRowFull : G.O.galleryLastRowFull;
6354      
6355      // gallery sorting
6356      G.gallerySorting.lN = G.O.gallerySorting.toUpperCase();
6357      G.gallerySorting.l1 = G.O.galleryL1Sorting != null ? G.O.galleryL1Sorting.toUpperCase() : G.gallerySorting.lN;
6358      
6359      // gallery display transition
6360      G.galleryDisplayTransition.lN = G.O.galleryDisplayTransition.toUpperCase();
6361      G.galleryDisplayTransition.l1 = G.O.galleryL1DisplayTransition != null ? G.O.galleryL1DisplayTransition.toUpperCase() : G.galleryDisplayTransition.lN;
6362
6363      // gallery display transition duration
6364      G.galleryDisplayTransitionDuration.lN = G.O.galleryDisplayTransitionDuration;
6365      G.galleryDisplayTransitionDuration.l1 = G.O.galleryL1DisplayTransitionDuration != null ? G.O.galleryL1DisplayTransitionDuration : G.galleryDisplayTransitionDuration.lN;
6366      
6367      // gallery max items per album (not for inline/api defined items)
6368      G.galleryMaxItems.lN = G.O.galleryMaxItems;
6369      G.galleryMaxItems.l1 = toType(G.O.galleryL1MaxItems) == 'number' ? G.O.galleryL1MaxItems : G.O.galleryMaxItems;
6370
6371      // gallery filter tags
6372      G.galleryFilterTags.lN = G.O.galleryFilterTags;
6373      G.galleryFilterTags.l1 = G.O.galleryL1FilterTags != null ? G.O.galleryL1FilterTags : G.O.galleryFilterTags;
6374      
6375      // gallery pagination
6376      G.O.galleryPaginationMode = G.O.galleryPaginationMode.toUpperCase();
6377
6378      if( toType(G.O.slideshowDelay) == 'number' && G.O.slideshowDelay >= 2000 ) {
6379        G.VOM.slideshowDelay = G.O.slideshowDelay;
6380      }
6381      else {
6382        NanoConsoleLog(G, 'Parameter "slideshowDelay" must be an integer >= 2000 ms.');
6383      }
6384
6385      // gallery display transition
6386      if( typeof G.O.thumbnailDisplayTransition == 'boolean' ) {
6387        if( G.O.thumbnailDisplayTransition === true ) {
6388          G.tn.opt.lN.displayTransition = 'FADEIN';
6389          G.tn.opt.l1.displayTransition = 'FADEIN';
6390        }
6391        else {
6392          G.tn.opt.lN.displayTransition = 'NONE';
6393          G.tn.opt.l1.displayTransition = 'NONE';
6394        }
6395      }
6396
6397      if( G.O.fnThumbnailDisplayEffect !== '' ) {
6398        G.tn.opt.lN.displayTransition = 'CUSTOM';
6399        G.tn.opt.l1.displayTransition = 'CUSTOM';
6400      }
6401      if( G.O.fnThumbnailL1DisplayEffect !== '' ) {
6402        G.tn.opt.l1.displayTransition = 'CUSTOM';
6403      }
6404      
6405      // parse thumbnail display transition
6406      function thumbnailDisplayTransitionParse( cfg, level ) {
6407        if( typeof cfg == 'string' ) {
6408          var st=cfg.split('_');
6409          if( st.length == 1 ) {
6410            G.tn.opt[level]['displayTransition'] = cfg.toUpperCase();
6411          }
6412          if( st.length == 2 ) {
6413            G.tn.opt[level]['displayTransition'] = st[0].toUpperCase();
6414            G.tn.opt[level]['displayTransitionStartVal'] = Number(st[1]);
6415          }
6416          if( st.length == 3 ) {
6417            G.tn.opt[level]['displayTransition'] = st[0].toUpperCase();
6418            G.tn.opt[level]['displayTransitionStartVal'] = Number(st[1]);
6419            G.tn.opt[level]['displayTransitionEasing'] = st[2];
6420          }
6421        }
6422      }
6423      thumbnailDisplayTransitionParse( G.O.thumbnailDisplayTransition, 'lN');
6424      thumbnailDisplayTransitionParse( G.O.thumbnailDisplayTransition, 'l1');
6425      thumbnailDisplayTransitionParse( G.O.thumbnailL1DisplayTransition, 'l1');
6426
6427      
6428      // thumbnail display transition duration
6429      ThumbnailOpt('thumbnailDisplayTransitionDuration', 'thumbnailL1DisplayTransitionDuration', 'displayTransitionDuration');
6430      // thumbnail display transition interval duration
6431      ThumbnailOpt('thumbnailDisplayInterval', 'thumbnailL1DisplayInterval', 'displayInterval');
6432
6433      
6434      // resolution breakpoints --> convert old syntax to new one
6435      if( G.O.thumbnailSizeSM !== undefined ) { G.O.breakpointSizeSM = G.O.thumbnailSizeSM; }
6436      if( G.O.thumbnailSizeME !== undefined ) { G.O.breakpointSizeME = G.O.thumbnailSizeME; }
6437      if( G.O.thumbnailSizeLA !== undefined ) { G.O.breakpointSizeLA = G.O.thumbnailSizeLA; }
6438      if( G.O.thumbnailSizeXL !== undefined ) { G.O.breakpointSizeXL = G.O.thumbnailSizeXL; }
6439
6440      // THUMBNAIL BUILD INIT
6441      //level 1
6442      if( G.O.thumbnailL1BuildInit2 !== undefined ) {
6443      var t1 = G.O.thumbnailL1BuildInit2.split('|');
6444        for( var i = 0; i < t1.length; i++ ) {
6445          var o1 = t1[i].trim().split('_');
6446          if( o1.length == 3 ) {
6447            var i1 = NewTBuildInit();
6448            i1.element = ThumbnailOverEffectsGetCSSElement(o1[0], '');
6449            i1.property = o1[1];
6450            i1.value = o1[2];
6451            G.tn.buildInit.level1.push(i1);
6452          }
6453        }
6454      }
6455      //level N
6456      if( G.O.thumbnailBuildInit2 !== undefined ) {
6457        var t1 = G.O.thumbnailBuildInit2.split('|');
6458        for( var i = 0; i < t1.length; i++ ) {
6459          var o1 = t1[i].trim().split('_');
6460          if( o1.length == 3 ) {
6461            var i1 = NewTBuildInit();
6462            i1.element = ThumbnailOverEffectsGetCSSElement(o1[0], '');
6463            i1.property = o1[1];
6464            i1.value = o1[2];
6465            G.tn.buildInit.std.push(i1);
6466          }
6467        }
6468      }
6469
6470      
6471      // THUMBNAIL HOVER EFFETCS
6472      
6473      // thumbnails hover effects - Level1
6474      var tL1HE = G.O.thumbnailL1HoverEffect2;
6475      if( tL1HE !== undefined ) {
6476        switch( toType(tL1HE) ) {
6477          case 'string':
6478            var tmp = tL1HE.split('|');
6479            for(var i = 0; i < tmp.length; i++) {
6480              var oDef = NewTHoverEffect();
6481              oDef = ThumbnailHoverEffectExtract( tmp[i].trim(), oDef );
6482              if(  oDef != null ) {
6483                G.tn.hoverEffects.level1.push(oDef);
6484              }
6485            }
6486            break;
6487          case 'object':
6488            var oDef = NewTHoverEffect();
6489            oDef = jQuery.extend(oDef,tL1HE);
6490            oDef = ThumbnailHoverEffectExtract( oDef.name, oDef );
6491            if(  oDef != null ) {
6492              G.tn.hoverEffects.level1.push(oDef);
6493            }
6494            break;
6495          case 'array':
6496            for(var i = 0; i < tL1HE.length; i++) {
6497              var oDef = NewTHoverEffect();
6498              oDef = jQuery.extend(oDef,tL1HE[i]);
6499              oDef = ThumbnailHoverEffectExtract( oDef.name, oDef );
6500              if(  oDef != null ) {
6501                G.tn.hoverEffects.level1.push(oDef);
6502              }
6503            }
6504            break;
6505          case 'null':
6506            break;
6507          default:
6508            NanoAlert(G, 'incorrect parameter for "thumbnailL1HoverEffect2".');
6509        }
6510      }
6511      G.tn.hoverEffects.level1 = ThumbnailOverEffectsPreset(G.tn.hoverEffects.level1);
6512  
6513      // thumbnails hover effects - other levels
6514      var tHE = G.O.thumbnailHoverEffect2;
6515      switch( toType(tHE) ) {
6516        case 'string':
6517          var tmp = tHE.split('|');
6518          for(var i = 0; i < tmp.length; i++) {
6519            var oDef = NewTHoverEffect();
6520            oDef = ThumbnailHoverEffectExtract( tmp[i].trim(), oDef );
6521            if(  oDef != null ) {
6522              G.tn.hoverEffects.std.push(oDef);
6523            }
6524          }
6525          break;
6526        case 'object':
6527          var oDef = NewTHoverEffect();
6528          oDef = jQuery.extend(oDef, tHE);
6529          oDef = ThumbnailHoverEffectExtract( oDef.name, oDef );
6530          if(  oDef != null ) {
6531            G.tn.hoverEffects.std.push(oDef);
6532          }
6533          break;
6534        case 'array':
6535          for(var i = 0; i < tHE.length; i++) {
6536            var oDef = NewTHoverEffect();
6537            oDef = jQuery.extend(oDef,tHE[i]);
6538            oDef = ThumbnailHoverEffectExtract( oDef.name, oDef );
6539            if(  oDef!= null ) {
6540              G.tn.hoverEffects.std.push(oDef);
6541            }
6542          }
6543          break;
6544        case 'null':
6545          break;
6546        default:
6547          NanoAlert(G, 'incorrect parameter for "thumbnailHoverEffect2".');
6548      }
6549      G.tn.hoverEffects.std = ThumbnailOverEffectsPreset(G.tn.hoverEffects.std);
6550
6551      
6552      if( G.O.touchAnimationL1 == undefined ) {
6553        G.O.touchAnimationL1 = G.O.touchAnimation;
6554      }
6555      
6556      // disable thumbnail touch animation when no hover effect defined
6557      if( G.tn.hoverEffects.std.length == 0 ) {
6558        if( G.tn.hoverEffects.level1.length == 0 ) {
6559          G.O.touchAnimationL1 = false;
6560        }
6561        G.O.touchAnimation = false;
6562      }      
6563      
6564      
6565      // thumbnail sizes
6566      if( G.O.thumbnailHeight == 0 || G.O.thumbnailHeight == ''  ) { G.O.thumbnailHeight = 'auto'; }
6567      if( G.O.thumbnailWidth == 0 || G.O.thumbnailWidth == '' ) { G.O.thumbnailWidth = 'auto'; }
6568      if( G.O.thumbnailL1Height == 0 || G.O.thumbnailL1Height == '' ) { G.O.thumbnailL1Height = 'auto'; }
6569      if( G.O.thumbnailL1Width == 0 || G.O.thumbnailL1Width == '' ) { G.O.thumbnailL1Width = 'auto'; }
6570
6571      // RETRIEVE ALL THUMBNAIL SIZES
6572      function ThumbnailSizes( srcOpt, onlyl1, opt) {
6573        if( G.O[srcOpt] == null ) { return; }
6574        
6575        if( toType(G.O[srcOpt]) == 'number' ) {
6576          ThumbnailsSetSize( opt, 'l1', G.O[srcOpt], 'u');
6577          if( !onlyl1 ) {
6578            ThumbnailsSetSize( opt, 'lN', G.O[srcOpt], 'u');
6579          }
6580        }
6581        else {
6582          var ws=G.O[srcOpt].split(' ');
6583          var v = 'auto';
6584          if( ws[0].substring(0,4) != 'auto' ) { v=parseInt(ws[0]); }
6585          var c = 'u';
6586          if( ws[0].charAt(ws[0].length - 1) == 'C' ) { c='c'; }
6587          ThumbnailsSetSize( opt, 'l1', v, c );   // default value for all resolutions and navigation levels
6588          if( !onlyl1 ) {
6589            ThumbnailsSetSize( opt, 'lN', v, c );
6590          }
6591          for( var i = 1; i < ws.length; i++ ) {
6592            var r = ws[i].substring(0,2).toLowerCase();
6593            if( /xs|sm|me|la|xl/i.test(r) ) {
6594              var w = ws[i].substring(2);
6595              var v = 'auto';
6596              if( w.substring(0,4) != 'auto' ) { v = parseInt(w); }
6597              var c = 'u';
6598              if( w.charAt(w.length - 1) == 'C' ) { c = 'c'; }
6599              G.tn.settings[opt]['l1'][r] = v;
6600              G.tn.settings[opt]['l1'][r + 'c'] = c;
6601              if( !onlyl1 ) {
6602                G.tn.settings[opt]['lN'][r] = v;
6603                G.tn.settings[opt]['lN'][r + 'c'] = c;
6604              }
6605            }
6606          }
6607        }
6608      }
6609      ThumbnailSizes( 'thumbnailWidth', false, 'width');
6610      ThumbnailSizes( 'thumbnailL1Width', true, 'width');
6611      
6612      ThumbnailSizes( 'thumbnailHeight', false, 'height');
6613      ThumbnailSizes( 'thumbnailL1Height', true, 'height');
6614
6615      
6616      G.O.thumbnailBorderHorizontal = parseInt(G.O.thumbnailBorderHorizontal);
6617      G.O.thumbnailBorderVertical = parseInt(G.O.thumbnailBorderVertical);
6618      G.O.thumbnailLabelHeight = parseInt(G.O.thumbnailLabelHeight);
6619
6620      
6621      // retrieve all mosaic layout patterns
6622      // default pattern
6623      if( G.O.galleryMosaic != undefined ) {
6624        // clone object
6625        G.tn.settings.mosaic.l1.xs = JSON.parse(JSON.stringify(G.O.galleryMosaic));
6626        G.tn.settings.mosaic.l1.sm = JSON.parse(JSON.stringify(G.O.galleryMosaic));
6627        G.tn.settings.mosaic.l1.me = JSON.parse(JSON.stringify(G.O.galleryMosaic));
6628        G.tn.settings.mosaic.l1.la = JSON.parse(JSON.stringify(G.O.galleryMosaic));
6629        G.tn.settings.mosaic.l1.xl = JSON.parse(JSON.stringify(G.O.galleryMosaic));
6630        G.tn.settings.mosaic.lN.xs = JSON.parse(JSON.stringify(G.O.galleryMosaic));
6631        G.tn.settings.mosaic.lN.sm = JSON.parse(JSON.stringify(G.O.galleryMosaic));
6632        G.tn.settings.mosaic.lN.me = JSON.parse(JSON.stringify(G.O.galleryMosaic));
6633        G.tn.settings.mosaic.lN.la = JSON.parse(JSON.stringify(G.O.galleryMosaic));
6634        G.tn.settings.mosaic.lN.xl = JSON.parse(JSON.stringify(G.O.galleryMosaic));
6635        G.tn.settings.mosaicCalcFactor('l1', 'xs');
6636        G.tn.settings.mosaicCalcFactor('l1', 'sm');
6637        G.tn.settings.mosaicCalcFactor('l1', 'me');
6638        G.tn.settings.mosaicCalcFactor('l1', 'la');
6639        G.tn.settings.mosaicCalcFactor('l1', 'xl');
6640        G.tn.settings.mosaicCalcFactor('lN', 'xs');
6641        G.tn.settings.mosaicCalcFactor('lN', 'sm');
6642        G.tn.settings.mosaicCalcFactor('lN', 'me');
6643        G.tn.settings.mosaicCalcFactor('lN', 'la');
6644        G.tn.settings.mosaicCalcFactor('lN', 'xl');
6645      }
6646      if( G.O.galleryL1Mosaic != undefined ) {
6647        // default L1 pattern
6648        G.tn.settings.mosaic.l1.xs = JSON.parse(JSON.stringify(G.O.galleryL1Mosaic));
6649        G.tn.settings.mosaic.l1.sm = JSON.parse(JSON.stringify(G.O.galleryL1Mosaic));
6650        G.tn.settings.mosaic.l1.me = JSON.parse(JSON.stringify(G.O.galleryL1Mosaic));
6651        G.tn.settings.mosaic.l1.la = JSON.parse(JSON.stringify(G.O.galleryL1Mosaic));
6652        G.tn.settings.mosaic.l1.xl = JSON.parse(JSON.stringify(G.O.galleryL1Mosaic));
6653        G.tn.settings.mosaicCalcFactor('l1', 'xs');
6654        G.tn.settings.mosaicCalcFactor('l1', 'sm');
6655        G.tn.settings.mosaicCalcFactor('l1', 'me');
6656        G.tn.settings.mosaicCalcFactor('l1', 'la');
6657        G.tn.settings.mosaicCalcFactor('l1', 'xl');
6658      }
6659      for( var w = 0; w < G.tn.settings.mosaic.l1; w++ ) {
vendor: 83,618 bytes, lines 6660-8722
6660        if( G.O['galleryMosaic' + G.tn.settings.mosaic.l1[w].toUpperCase()] != undefined ) {
6661          G.tn.settings.mosaic.lN[tn.settings.mosaic.l1[w]] = JSON.parse(JSON.stringify( G.O['galleryMosaic' + G.tn.settings.mosaic.l1[w].toUpperCase()] ));
6662          G.tn.settings.mosaic.l1[tn.settings.mosaic.l1[w]] = JSON.parse(JSON.stringify( G.O['galleryMosaic' + G.tn.settings.mosaic.l1[w].toUpperCase()] ));
6663          G.tn.settings.mosaicCalcFactor('l1', G.tn.settings.mosaic.l1[w]);
6664          G.tn.settings.mosaicCalcFactor('lN', G.tn.settings.mosaic.l1[w]);
6665        }
6666      }
6667      for( var w = 0; w < G.tn.settings.mosaic.l1; w++ ) {
6668        if( G.O['galleryL1Mosaic'+G.tn.settings.mosaic.l1[w].toUpperCase()] != undefined ) {
6669          G.tn.settings.mosaic.l1[tn.settings.mosaic.l1[w]] = JSON.parse(JSON.stringify( G.O['galleryL1Mosaic'+G.tn.settings.mosaic.l1[w].toUpperCase()] ));
6670          G.tn.settings.mosaicCalcFactor('l1', G.tn.settings.mosaic.l1[w]);
6671        }
6672      }
6673      
6674      G.O.imageTransition = G.O.imageTransition.toUpperCase();
6675
6676      G.layout.SetEngine();
6677      
6678      // init plugins
6679      switch( G.O.kind ) {
6680        // MARKUP / API
6681        case '':
6682          break;
6683        // JSON, Flickr, Picasa, ...
6684        default:
6685        jQuery.nanogallery2['data_' + G.O.kind](G, 'Init' );
6686      }
6687
6688    }
6689
6690    // HOVER EFFECTS
6691    function ThumbnailHoverEffectExtract( name, effect) {
6692      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'];
6693    
6694      var sp = name.split('_');
6695      if( sp.length >= 4 ) {
6696        // var oDef=NewTHoverEffect();
6697        effect.name = '';
6698        effect.type = sp[1];
6699        effect.from = sp[2];
6700        effect.to = sp[3];
6701        if( sp.length >= 5 ) {
6702          // effect.duration=sp[4];
6703
6704          for( var n = 4; n < sp.length; n++ ) {
6705            var v = sp[n];
6706            
6707            // check if an easing name
6708            var foundEasing = false;
6709            for( var e = 0; e < easings.length; e++) {
6710              if( v == easings[e] ) {
6711                foundEasing = true;
6712                effect.easing = v;
6713                break;
6714              }
6715            }
6716            if( foundEasing === true ) {
6717              continue;
6718            }
6719            
6720            v = v.toUpperCase();
6721            
6722            if( v == 'HOVERIN' ) {
6723              effect.hoverout = false;
6724              continue;
6725            }
6726            if( v == 'HOVEROUT' ) {
6727              effect.hoverin = false;
6728              continue;
6729            }
6730            
6731            if( v == 'KEYFRAME' ) {
6732              effect.firstKeyframe = false;
6733              continue;
6734            }
6735            
6736            var num = parseInt(v.replace(/[^0-9\.]/g, ''), 10);   // extract a number if one exists
6737
6738            if( num > 0 ) {
6739              // the string contains a numbers > 0
6740              if( v.indexOf('DURATION') >= 0 ) {
6741                effect.duration = num;
6742                continue;
6743              }
6744              if( v.indexOf('DURATIONBACK') >= 0 ) {
6745                effect.durationBack = num;
6746                continue;
6747              }
6748              if( v.indexOf('DELAY') >= 0 ) {
6749                effect.delay = num;
6750                continue;
6751              }
6752              if( v.indexOf('DELAYBACK') >= 0 ) {
6753                effect.delayBack = num;
6754                continue;
6755              }
6756              
6757              // no parameter name found -> default is duration
6758              effect.duration = num;
6759            }
6760          }
6761        }        
6762        effect.element=ThumbnailOverEffectsGetCSSElement(sp[0], effect.type);
6763        
6764      }
6765      else {
6766        effect.name = name;
6767        // NanoAlert(G, 'incorrect parameter for "thumbnailHoverEffect": ' + name);
6768        // return null;
6769      }
6770      return effect;
6771    }
6772    
6773    
6774    function ThumbnailOverEffectsGetCSSElement( element, property ) {
6775        var r = element;
6776        
6777        switch ( element ) {
6778          case 'image':
6779            if( property == 'blur' || property == 'brightness' || property == 'grayscale' || property == 'sepia' || property == 'contrast' || property == 'opacity'|| property == 'saturate' ) {
6780              // r='.nGY2GThumbnailImg';
6781              r = '.nGY2GThumbnailImage';
6782            }
6783            else {
6784              r = '.nGY2GThumbnailImage';
6785            }
6786            break;
6787          case 'thumbnail':
6788            r = '.nGY2GThumbnail';
6789            break;
6790          case 'label':
6791            r = '.nGY2GThumbnailLabel';
6792            break;
6793          case 'title':
6794            r = '.nGY2GThumbnailTitle';
6795            break;
6796          case 'description':
6797            r = '.nGY2GThumbnailDescription';
6798            break;
6799          case 'tools':
6800            r = '.nGY2GThumbnailIcons';
6801            break;
6802          case 'customlayer':
6803            r = '.nGY2GThumbnailCustomLayer';
6804            break;
6805        }
6806        return r;
6807    }
6808    
6809    // convert preset hover effects to new ones (nanogallery2)
6810    function ThumbnailOverEffectsPreset( effects ) {
6811
6812      // COMPATIBILITY WITH nanoGALLERY
6813      // OK:
6814      //  'borderLighter', 'borderDarker', 'scale120', 'labelAppear', 'labelAppear75', 'labelOpacity50', 'scaleLabelOverImage'
6815      //  'overScale', 'overScaleOutside', 'descriptionAppear'
6816      //  'slideUp', 'slideDown', 'slideRight', 'slideLeft'
6817      //  'imageScale150', 'imageScaleIn80', 'imageScale150Outside', 'imageSlideUp', 'imageSlideDown', 'imageSlideRight', 'imageSlideLeft'
6818      //  'labelSlideUpTop', 'labelSlideUp', 'labelSlideDown', 'descriptionSlideUp'
6819      // KO:
6820      //  'labelSplit4', 'labelSplitVert', 'labelAppearSplit4', 'labelAppearSplitVert' 
6821      // TODO:
6822      //  'rotateCornerBL', 'rotateCornerBR', 'imageSplit4', 'imageSplitVert', 'imageRotateCornerBL', 'imageRotateCornerBR', 'imageFlipHorizontal', 'imageFlipVertical'
6823
6824    
6825   
6826      var newEffects=[];
6827      for( var i=0; i< effects.length; i++ ) {
6828        switch( effects[i].name.toUpperCase() ) {
6829          case 'BORDERLIGHTER':
6830            // var color=ngtinycolor(GalleryThemeGetCurrent().thumbnail.borderColor);
6831            // name='thumbnail_borderColor_'+color.toRgbString()+'_'+color.lighten(50).toRgbString();
6832            
6833            var rgb = ColorHelperToRGB(GalleryThemeGetCurrent().thumbnail.borderColor);
6834            name = 'thumbnail_borderColor_'+rgb+'_'+ShadeBlendConvert(0.5, rgb );
6835            newEffects.push(ThumbnailHoverEffectExtract(name, effects[i]));
6836            break;
6837          case 'BORDERDARKER':
6838            // var color=ngtinycolor(GalleryThemeGetCurrent().thumbnail.borderColor);
6839            // name='thumbnail_borderColor_'+color.toRgbString()+'_'+color.darken(50).toRgbString();
6840            var rgb = ColorHelperToRGB(GalleryThemeGetCurrent().thumbnail.borderColor);
6841            name = 'thumbnail_borderColor_'+rgb+'_'+ShadeBlendConvert(-0.5, rgb );
6842            newEffects.push(ThumbnailHoverEffectExtract(name, effects[i]));
6843            break;
6844          case 'SCALE120':
6845            newEffects.push(ThumbnailHoverEffectExtract('thumbnail_scale_1.00_1.20', effects[i]));
6846            break;
6847          case 'LABELAPPEAR':
6848          case 'LABELAPPEAR75':
6849            newEffects.push(ThumbnailHoverEffectExtract('label_opacity_0.00_1.00', effects[i]));
6850            break;
6851          case 'TOOLSAPPEAR':
6852            newEffects.push(ThumbnailHoverEffectExtract('tools_opacity_0_1', effects[i]));
6853            break;
6854          case 'TOOLSSLIDEDOWN':
6855            newEffects.push(ThumbnailHoverEffectExtract('tools_translateY_-100%_0%', effects[i]));
6856            break;
6857          case 'TOOLSSLIDEUP':
6858            newEffects.push(ThumbnailHoverEffectExtract('tools_translateY_100%_0%', effects[i]));
6859            break;
6860          case 'LABELOPACITY50':
6861            newEffects.push(ThumbnailHoverEffectExtract('label_opacity_1.00_0.50', effects[i]));
6862            break;
6863          case 'LABELSLIDEUPTOP':
6864          case 'LABELSLIDEUP':
6865            newEffects.push(ThumbnailHoverEffectExtract('label_translateY_100%_0%', effects[i]));
6866            break;
6867          case 'LABELSLIDEDOWN':
6868            newEffects.push(ThumbnailHoverEffectExtract('label_translateY_-100%_0%', effects[i]));
6869            break;
6870          case 'SCALELABELOVERIMAGE':
6871            newEffects.push(ThumbnailHoverEffectExtract('label_scale_0.00_1.00', effects[i]));
6872            var n = cloneJSObject(effects[i]);
6873            newEffects.push(ThumbnailHoverEffectExtract('image_scale_1.00_0.00', n));
6874            break;
6875          case 'OVERSCALE':
6876          case 'OVERSCALEOUTSIDE':
6877            name = 'label_scale_0_100';
6878            newEffects.push(ThumbnailHoverEffectExtract('label_scale_2.00_1.00', effects[i]));
6879            var n = cloneJSObject(effects[i]);
6880            newEffects.push(ThumbnailHoverEffectExtract('label_opacity_0.00_1.00', n));
6881            n = cloneJSObject(effects[i]);
6882            newEffects.push(ThumbnailHoverEffectExtract('image_scale_1.00_0.00', n));
6883            n = cloneJSObject(effects[i]);
6884            newEffects.push(ThumbnailHoverEffectExtract('image_opacity_1.00_0.00', n));
6885            break;
6886          case 'DESCRIPTIONAPPEAR':
6887            newEffects.push(ThumbnailHoverEffectExtract('description_opacity_0_1', effects[i]));
6888            break;
6889          case 'SLIDERIGHT':
6890            newEffects.push(ThumbnailHoverEffectExtract('image_translateX_0%_100%', effects[i]));
6891            newEffects.push(ThumbnailHoverEffectExtract('label_translateX_-100%_0%', cloneJSObject(effects[i])));
6892            break;
6893          case 'SLIDELEFT':
6894            newEffects.push(ThumbnailHoverEffectExtract('image_translateX_0%_-100%', effects[i]));
6895            newEffects.push(ThumbnailHoverEffectExtract('label_translateX_100%_0%', cloneJSObject(effects[i])));
6896            break;
6897          case 'SLIDEUP':
6898            newEffects.push(ThumbnailHoverEffectExtract('image_translateY_0%_-100%', effects[i]));
6899            newEffects.push(ThumbnailHoverEffectExtract('label_translateY_100%_0%', cloneJSObject(effects[i])));
6900            break;
6901          case 'SLIDEDOWN':
6902            newEffects.push(ThumbnailHoverEffectExtract('image_translateY_0%_100%', effects[i]));
6903            newEffects.push(ThumbnailHoverEffectExtract('label_translateY_-100%_0%', cloneJSObject(effects[i])));
6904            break;
6905          case 'IMAGESCALE150':
6906          case 'IMAGESCALE150OUTSIDE':
6907            newEffects.push(ThumbnailHoverEffectExtract('image_scale_1.00_1.50', effects[i]));
6908            break;
6909          case 'IMAGESCALEIN80':
6910            newEffects.push(ThumbnailHoverEffectExtract('image_scale_1.20_1.00', effects[i]));
6911            break;
6912          case 'IMAGESLIDERIGHT':
6913            newEffects.push(ThumbnailHoverEffectExtract('image_translateX_0%_100%', effects[i]));
6914            break;
6915          case 'IMAGESLIDELEFT':
6916            newEffects.push(ThumbnailHoverEffectExtract('image_translateX_0%_-100%', effects[i]));
6917            break;
6918          case 'IMAGESLIDEUP':
6919            newEffects.push(ThumbnailHoverEffectExtract('image_translateY_0%_-100%', effects[i]));
6920            break;
6921          case 'IMAGESLIDEDOWN':
6922            newEffects.push(ThumbnailHoverEffectExtract('image_translateY_0%_100%', effects[i]));
6923            break;
6924          case 'LABELSLIDEUP':
6925          case 'LABELSLIDEUPTOP':
6926            newEffects.push(ThumbnailHoverEffectExtract('label_translateY_100%_0%', effects[i]));
6927            break;
6928          case 'LABELSLIDEUPDOWN':
6929            newEffects.push(ThumbnailHoverEffectExtract('label_translateY_0%_100%', effects[i]));
6930            break;
6931          case 'DESCRIPTIONSLIDEUP':
6932            newEffects.push(ThumbnailHoverEffectExtract('description_translateY_110%_0%', effects[i]));
6933            break;
6934
6935          case 'IMAGEBLURON':
6936            newEffects.push(ThumbnailHoverEffectExtract('image_blur_2.00px_0.00px', effects[i]));
6937            break;
6938          case 'IMAGEBLUROFF':
6939            newEffects.push(ThumbnailHoverEffectExtract('image_blur_0.00px_2.00px', effects[i]));
6940            break;
6941          case 'IMAGEGRAYON':
6942            newEffects.push(ThumbnailHoverEffectExtract('image_grayscale_0%_100%', effects[i]));
6943            break;
6944          case 'IMAGEGRAYOFF':
6945            newEffects.push(ThumbnailHoverEffectExtract('image_grayscale_100%_0%', effects[i]));
6946            break;
6947          case 'IMAGESEPIAON':
6948            newEffects.push(ThumbnailHoverEffectExtract('image_sepia_100%_1%', effects[i]));
6949            break;
6950          case 'IMAGESEPIAOFF':
6951            newEffects.push(ThumbnailHoverEffectExtract('image_sepia_1%_100%', effects[i]));
6952            break;
6953            
6954          default:
6955            newEffects.push(effects[i]);
6956            break;
6957        }
6958      }
6959      
6960      return newEffects;
6961    }
6962
6963
6964    // Thumbnail hover effect definition
6965    function NewTHoverEffect() {
6966      var oDef={ 
6967          name:           '',
6968          element:        '',               // element class
6969          type:           '',               
6970          from:           '',               // start value
6971          to:             '',               // end value
6972          hoverin:        true,
6973          hoverout:       true,
6974          firstKeyframe:  true,
6975          delay:          0,
6976          delayBack:      0,
6977          duration:       400,
6978          durationBack:   300,
6979          easing:         'easeOutQuart',
6980          easingBack:     'easeOutQuart',
6981          animParam:      null
6982        };
6983      return oDef;
6984    }
6985
6986    function NewTBuildInit() {
6987      // to set CSS properties
6988      var oDef={ element: '', property: '', value: '' };
6989      return oDef;
6990    }
6991
6992    
6993    function ThumbnailStyle( cfg, level) {
6994    
6995      switch( cfg.position ){
6996        case 'onBottom' :
6997          G.tn.style[level]['label'] = 'bottom:0; ';
6998          break;
6999        case 'overImageOnTop' :
7000          G.tn.style[level]['label'] = 'top:0; position:absolute;';
7001          break;
7002        case 'overImageOnMiddle' :
7003          G.tn.style[level]['label'] = 'top:0; bottom:0;';
7004          G.tn.style[level]['title'] = 'position:absolute; bottom:50%;';
7005          G.tn.style[level]['desc'] = 'position:absolute; top:50%;';
7006          break;
7007        case 'custom' :
7008          break;
7009        case 'overImageOnBottom' :
7010        default :
7011          G.O.thumbnailLabel.position = 'overImageOnBottom';
7012          G.tn.style[level].label = 'bottom:0; position:absolute;';
7013          break;
7014      }
7015      // if( G.layout.engine != 'CASCADING' ) {
7016      if( cfg.position != 'onBottom' ) {
7017        // multi-line
7018        if( cfg.titleMultiLine ) {
7019          G.tn.style[level]['title'] = 'white-space:normal;';
7020        }
7021        if( cfg.descriptionMultiLine ) {
7022          G.tn.style[level]['desc'] = 'white-space:normal;';
7023        }
7024      }
7025      
7026      
7027      switch( cfg.align ) {
7028        case 'right':
7029            G.tn.style[level].label += 'text-align:right;';
7030          break;
7031        case 'left':
7032            G.tn.style[level].label += 'text-align:left;';
7033          break;
7034        default:
7035            G.tn.style[level].label += 'text-align:center;';
7036          break;
7037      }
7038      if( cfg.titleFontSize != undefined && cfg.titleFontSize != '' ) {
7039        G.tn.style[level].title += 'font-size:' + cfg.titleFontSize + ';';
7040      }
7041      if( cfg.descriptionFontSize != undefined && cfg.descriptionFontSize != '' ) {
7042        G.tn.style[level].desc += 'font-size:' + cfg.descriptionFontSize + ';';
7043      }
7044      
7045      if( cfg.displayDescription == false ) {
7046        G.tn.style[level].desc += 'display:none;';
7047      }
7048    }
7049
7050    
7051    // cache some thumbnail settings
7052    function ThumbnailDefCaches() {
7053      // thumbnail content CSS styles
7054
7055      // settings for level L1 and LN
7056      ThumbnailStyle( G.O.thumbnailLabel, 'lN');
7057      ThumbnailStyle( G.O.thumbnailLabel, 'l1');
7058
7059      if( G.O.thumbnailL1Label && G.O.thumbnailL1Label.display ) {
7060        // settings for level L1
7061        ThumbnailStyle( G.O.thumbnailL1Label, 'l1');
7062      }
7063      
7064      G.tn.borderWidth = G.O.thumbnailBorderHorizontal;
7065      G.tn.borderHeight = G.O.thumbnailBorderVertical;
7066      
7067
7068      // default thumbnail sizes levels l1 and lN
7069      var lst=['xs','sm','me','la','xl'];
7070      for( var i = 0; i < lst.length; i++ ) {
7071        var w = G.tn.settings.width.lN[lst[i]];
7072        if( w != 'auto' ) {
7073          G.tn.defaultSize.width.lN[lst[i]] = w;
7074          G.tn.defaultSize.width.l1[lst[i]] = w;
7075        }
7076        else {
7077          var h = G.tn.settings.height.lN[lst[i]];
7078          G.tn.defaultSize.width.lN[lst[i]] = h;      // dynamic width --> set height value as default for the width
7079          G.tn.defaultSize.width.l1[lst[i]] = h;      // dynamic width --> set height value as default
7080        }
7081      }
7082      for( var i = 0; i < lst.length; i++ ) {
7083        var h = G.tn.settings.height.lN[lst[i]];
7084        if( h != 'auto' ) {
7085          // grid or justified layout
7086          G.tn.defaultSize.height.lN[lst[i]] = h;  //+G.tn.labelHeight.get();
7087          G.tn.defaultSize.height.l1[lst[i]] = h;  //+G.tn.labelHeight.get();
7088        }
7089        else {
7090          var w = G.tn.settings.width.lN[lst[i]];
7091          G.tn.defaultSize.height.lN[lst[i]] = w;      // dynamic height --> set width value as default for the height
7092          G.tn.defaultSize.height.l1[lst[i]] = w;      // dynamic height --> set width value as default
7093        }
7094      }
7095
7096      // default thumbnail sizes levels l1
7097      for( var i = 0; i < lst.length; i++ ) {
7098        var w = G.tn.settings.width.l1[lst[i]];
7099        if( w != 'auto' ) {
7100          G.tn.defaultSize.width.l1[lst[i]] = w;
7101        }
7102        else {
7103          var h = G.tn.settings.height.l1[lst[i]];
7104          G.tn.defaultSize.width.l1[lst[i]] = h;      // dynamic width --> set height value as default
7105        }
7106      }
7107      for( var i = 0; i < lst.length; i++ ) {
7108        var h = G.tn.settings.height.l1[lst[i]];
7109        if( h != 'auto' ) {
7110          // grid or justified layout
7111          G.tn.defaultSize.height.l1[lst[i]] = h;  //+G.tn.labelHeight.get();
7112        }
7113        else {
7114          var w = G.tn.settings.width.l1[lst[i]];
7115          G.tn.defaultSize.height.l1[lst[i]]= w ;      // dynamic height --> set width value as default
7116        }
7117      }
7118      
7119    }
7120    
7121
7122    // ##### THUMBNAIL SIZE MANAGEMENT
7123    function ThumbnailsSetSize( dir, level, v, crop ) {
7124      G.tn.settings[dir][level]['xs'] = v;
7125      G.tn.settings[dir][level]['sm'] = v;
7126      G.tn.settings[dir][level]['me'] = v;
7127      G.tn.settings[dir][level]['la'] = v;
7128      G.tn.settings[dir][level]['xl'] = v;
7129      G.tn.settings[dir][level]['xsc'] = crop;
7130      G.tn.settings[dir][level]['smc'] = crop;
7131      G.tn.settings[dir][level]['mec'] = crop;
7132      G.tn.settings[dir][level]['lac'] = crop;
7133      G.tn.settings[dir][level]['xlc'] = crop;
7134    }
7135
7136
7137    //
7138    function GalleryThemeGetCurrent() {
7139
7140      var cs=null;
7141      switch(toType(G.O.galleryTheme)) {
7142        case 'object':    // user custom color scheme object 
7143          cs = G.galleryTheme_dark;  // default color scheme
7144          jQuery.extend(true,cs,G.O.galleryTheme);
7145          break;
7146        case 'string':    // name of an internal defined color scheme
7147          switch( G.O.galleryTheme ) {
7148            case 'light':
7149              cs = G.galleryTheme_light;
7150              break;
7151            case 'default':
7152            case 'dark':
7153            case 'none':
7154            default:
7155              cs = G.galleryTheme_dark;
7156          }
7157          break;
7158        default:
7159          cs = G.galleryTheme_dark;
7160      }
7161      return cs;
7162    }
7163    
7164    // ##### BREADCRUMB/THUMBNAIL COLOR SCHEME #####
7165    function SetGalleryTheme() {
7166    
7167      if( typeof G.O.colorScheme  !== 'undefined' ) {
7168        G.O.galleryTheme = G.O.colorScheme;
7169      }
7170
7171      var cs = null;
7172      var galleryTheme = '';
7173      switch(toType(G.O.galleryTheme)) {
7174        case 'object':    // user custom color scheme object 
7175          cs = G.galleryTheme_dark;  // default color scheme
7176          jQuery.extend(true,cs,G.O.galleryTheme);
7177          galleryTheme='nanogallery_gallerytheme_custom_' + G.baseEltID;
7178          break;
7179        case 'string':    // name of an internal defined color scheme
7180          switch( G.O.galleryTheme ) {
7181            case 'light':
7182              cs = G.galleryTheme_light;
7183              galleryTheme='nanogallery_gallerytheme_light_' + G.baseEltID;
7184              break;
7185            case 'default':
7186            case 'dark':
7187            case 'none':
7188            default:
7189              cs = G.galleryTheme_dark;
7190              galleryTheme='nanogallery_gallerytheme_dark_' + G.baseEltID;
7191          }
7192          break;
7193        default:
7194          NanoAlert(G, 'Error in galleryTheme parameter.');
7195          return;
7196      }
7197
7198      //var s1='.nanogallery_theme_'+G.O.theme+' ';
7199      var s1='.' + galleryTheme + ' ';
7200    
7201      // navigation bar
7202      var s=s1+'.nGY2Navigationbar { background:'+cs.navigationBar.background+'; }'+'\n';
7203      if( cs.navigationBar.border !== undefined && cs.navigationBar.border !== '' ) { s+=s1+'.nGY2Navigationbar { border:'+cs.navigationBar.border+'; }'+'\n'; }
7204      if( cs.navigationBar.borderTop !== undefined && cs.navigationBar.borderTop !== '' ) { s+=s1+'.nGY2Navigationbar { border-top:'+cs.navigationBar.borderTop+'; }'+'\n'; }
7205      if( cs.navigationBar.borderBottom !== undefined && cs.navigationBar.borderBottom !== '' ) { s+=s1+'.nGY2Navigationbar { border-bottom:'+cs.navigationBar.borderBottom+'; }'+'\n'; }
7206      if( cs.navigationBar.borderRight !== undefined && cs.navigationBar.borderRight !== '' ) { s+=s1+'.nGY2Navigationbar { border-right:'+cs.navigationBar.borderRight+'; }'+'\n'; }
7207      if( cs.navigationBar.borderLeft !== undefined && cs.navigationBar.borderLeft !== '' ) { s+=s1+'.nGY2Navigationbar { border-left:'+cs.navigationBar.borderLeft+'; }'+'\n'; }
7208      
7209      // navigation bar - breadcrumb
7210      s+=s1+'.nGY2Breadcrumb { background:'+cs.navigationBreadcrumb.background+'; border-radius:'+cs.navigationBreadcrumb.borderRadius+'; }'+'\n';
7211      s+=s1+'.nGY2Breadcrumb .oneItem  { color:'+cs.navigationBreadcrumb.color+'; }'+'\n';
7212      s+=s1+'.nGY2Breadcrumb .oneItem:hover { color:'+cs.navigationBreadcrumb.colorHover+'; }'+'\n';
7213
7214      // navigation bar - tag filter
7215      s+=s1+'.nGY2NavFilterUnselected { color:'+cs.navigationFilter.color+'; background:'+cs.navigationFilter.background+'; border-radius:'+cs.navigationFilter.borderRadius+'; }'+'\n';
7216      s+=s1+'.nGY2NavFilterSelected { color:'+cs.navigationFilter.colorSelected+'; background:'+cs.navigationFilter.backgroundSelected+'; border-radius:'+cs.navigationFilter.borderRadius+'; }'+'\n';
7217      s+=s1+'.nGY2NavFilterSelectAll { color:'+cs.navigationFilter.colorSelected+'; background:'+cs.navigationFilter.background+'; border-radius:'+cs.navigationFilter.borderRadius+'; }'+'\n';
7218      
7219      // thumbnails
7220      s+=s1+'.nGY2GThumbnail { background:'+cs.thumbnail.background+'; border-color:'+cs.thumbnail.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';
7221      s+=s1+'.nGY2GThumbnailStack { background:'+cs.thumbnail.stackBackground+'; }'+'\n';
7222      // s+=s1+'.nGY2GThumbnailImage { background:'+cs.thumbnail.background+'; background-image:'+cs.thumbnail.backgroundImage+'; }'+'\n';
7223      s+=s1+'.nGY2TnImgBack { background:'+cs.thumbnail.background+'; background-image:'+cs.thumbnail.backgroundImage+'; }'+'\n';
7224      s+=s1+'.nGY2GThumbnailAlbumUp { background:'+cs.thumbnail.background+'; background-image:'+cs.thumbnail.backgroundImage+'; color:'+cs.thumbnail.titleColor+'; }'+'\n';
7225      s+=s1+'.nGY2GThumbnailIconsFullThumbnail { color:'+cs.thumbnail.titleColor+'; }\n';
7226      s+=s1+'.nGY2GThumbnailLabel { background:'+cs.thumbnail.labelBackground+'; opacity:'+cs.thumbnail.labelOpacity+'; }'+'\n';
7227      s+=s1+'.nGY2GThumbnailImageTitle  { color:'+cs.thumbnail.titleColor+'; background-color:'+cs.thumbnail.titleBgColor+'; '+(cs.thumbnail.titleShadow =='' ? '': 'Text-Shadow:'+cs.thumbnail.titleShadow+';')+' }'+'\n';
7228      s+=s1+'.nGY2GThumbnailAlbumTitle { color:'+cs.thumbnail.titleColor+'; background-color:'+cs.thumbnail.titleBgColor+'; '+(cs.thumbnail.titleShadow =='' ? '': 'Text-Shadow:'+cs.thumbnail.titleShadow+';')+' }'+'\n';
7229      s+=s1+'.nGY2GThumbnailDescription { color:'+cs.thumbnail.descriptionColor+'; background-color:'+cs.thumbnail.descriptionBgColor+'; '+(cs.thumbnail.descriptionShadow =='' ? '': 'Text-Shadow:'+cs.thumbnail.descriptionShadow+';')+' }'+'\n';
7230
7231      // thumbnails - icons
7232      s+=s1+'.nGY2GThumbnailIcons { padding:'+cs.thumbnailIcon.padding+'; }\n';
7233      s+=s1+'.nGY2GThumbnailIcon { color:'+cs.thumbnailIcon.color+'; }\n';
7234      s+=s1+'.nGY2GThumbnailIconTextBadge { background-color:'+cs.thumbnailIcon.color+'; }\n';
7235      
7236      // gallery pagination -> dot/rectangle based
7237      if( G.O.galleryPaginationMode != 'NUMBERS' ) {
7238        s+=s1+'.nGY2paginationDot { border:'+cs.pagination.shapeBorder+'; background:'+cs.pagination.shapeColor+';}\n';
7239        s+=s1+'.nGY2paginationDotCurrentPage { border:'+cs.pagination.shapeBorder+'; background:'+cs.pagination.shapeSelectedColor+';}\n';
7240        s+=s1+'.nGY2paginationRectangle { border:'+cs.pagination.shapeBorder+'; background:'+cs.pagination.shapeColor+';}\n';
7241        s+=s1+'.nGY2paginationRectangleCurrentPage { border:'+cs.pagination.shapeBorder+'; background:'+cs.pagination.shapeSelectedColor+';}\n';
7242      } else {
7243        s+=s1+'.nGY2paginationItem { background:'+cs.pagination.background+'; color:'+cs.pagination.color+'; border-radius:'+cs.pagination.borderRadius+'; }\n';
7244        s+=s1+'.nGY2paginationItemCurrentPage { background:'+cs.pagination.background+'; color:'+cs.pagination.color+'; border-radius:'+cs.pagination.borderRadius+'; }\n';
7245        s+=s1+'.nGY2PaginationPrev { background:'+cs.pagination.background+'; color:'+cs.pagination.color+'; border-radius:'+cs.pagination.borderRadius+'; }\n';
7246        s+=s1+'.nGY2PaginationNext { background:'+cs.pagination.background+'; color:'+cs.pagination.color+'; border-radius:'+cs.pagination.borderRadius+'; }\n';
7247        s+=s1+'.nGY2paginationItemCurrentPage { background:'+cs.pagination.backgroundSelected+'; }\n';
7248      }
7249      
7250      // gallery more button
7251      s+=s1+'.nGY2GalleryMoreButtonAnnotation { background:'+cs.thumbnail.background+'; border-color:'+cs.thumbnail.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';
7252      s+=s1+'.nGY2GalleryMoreButtonAnnotation  { color:'+cs.thumbnail.titleColor+'; '+(cs.thumbnail.titleShadow =='' ? '': 'Text-Shadow:'+cs.thumbnail.titleShadow)+'; }\n';
7253      
7254      jQuery('head').append('<style id="ngycs_'+G.baseEltID+'">'+s+'</style>');
7255      G.$E.base.addClass(galleryTheme);
7256
7257    };
7258    
7259    // ##### VIEWER COLOR SCHEME #####
7260    function SetViewerTheme( ) {
7261
7262      if( G.VOM.viewerTheme != '' ) {
7263        G.VOM.$cont.addClass(G.VOM.viewerTheme);
7264        return;
7265      }
7266
7267      if( typeof G.O.colorSchemeViewer  !== 'undefined' ) {
7268        G.O.viewerTheme = G.O.colorSchemeViewer;
7269      }
7270
7271      var cs=null;
7272      switch(toType(G.O.viewerTheme)) {
7273        case 'object':    // user custom color scheme object 
7274          cs = G.viewerTheme_dark;
7275          jQuery.extend(true, cs, G.O.viewerTheme);
7276          G.VOM.viewerTheme = 'nanogallery_viewertheme_custom_' + G.baseEltID;
7277          break;
7278        case 'string':    // name of an internal defined color scheme
7279          switch( G.O.viewerTheme ) {
7280            case 'none':
7281              return;
7282              break;
7283            case 'light':
7284              cs = G.viewerTheme_light;
7285              G.VOM.viewerTheme = 'nanogallery_viewertheme_light_' + G.baseEltID;
7286              break;
7287            case 'border':
7288              cs = G.viewerTheme_border;
7289              G.VOM.viewerTheme = 'nanogallery_viewertheme_border_' + G.baseEltID;
7290              break;
7291            case 'dark':
7292            case 'default':
7293              cs = G.viewerTheme_dark;
7294              G.VOM.viewerTheme = 'nanogallery_viewertheme_dark_' + G.baseEltID;
7295              break;
7296          }
7297          break;
7298        default:
7299          NanoAlert(G, 'Error in viewerTheme parameter.');
7300          return;
7301      }
7302
7303      var s1 = '.' + G.VOM.viewerTheme + ' ';
7304      var s = s1 + '.nGY2Viewer { background:' + cs.background + '; }'+'\n';
7305      s += s1 + '.nGY2ViewerMedia { border:' + cs.imageBorder + '; box-shadow:' + cs.imageBoxShadow + '; }'+'\n';
7306      s += s1 + '.nGY2Viewer .toolbarBackground { background:' + cs.barBackground + '; }'+'\n';
7307      s += s1 + '.nGY2Viewer .nGY2ViewerToolsTopLeft { background:' + cs.barBackground + '; }'+'\n';
7308      s += s1 + '.nGY2Viewer .nGY2ViewerToolsTopRight { background:' + cs.barBackground + '; }'+'\n';
7309      s += s1 + '.nGY2Viewer .nGY2ViewerAreaNext { background:' + cs.barBackground + '; }'+'\n';
7310      s += s1 + '.nGY2Viewer .nGY2ViewerAreaPrevious { background:' + cs.barBackground + '; }'+'\n';
7311      s += s1 + '.nGY2Viewer .toolbar { border:' + cs.barBorder + '; color:' + cs.barColor + '; }'+'\n';
7312      s += s1 + '.nGY2Viewer .toolbar .previousButton:after { color:' + cs.barColor + '; }'+'\n';
7313      s += s1 + '.nGY2Viewer .toolbar .nextButton:after { color:' + cs.barColor + '; }'+'\n';
7314      s += s1 + '.nGY2Viewer .toolbar .closeButton:after { color:' + cs.barColor + '; }'+'\n';
7315      s += s1 + '.nGY2Viewer .toolbar .label .title { color:' + cs.barColor + '; }'+'\n';
7316      s += s1 + '.nGY2Viewer .toolbar .label .description { color:' + cs.barDescriptionColor + '; }'+'\n';
7317      jQuery('head').append('<style>' + s + '</style>');
7318      G.VOM.$cont.addClass(G.VOM.viewerTheme);
7319    };
7320
7321    
7322    
7323    /** @function SetPolyFills */
7324    function SetPolyFills() {
7325
7326      // POLYFILL FOR BIND function --> for older Safari mobile
7327      // found on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility
7328      if (!Function.prototype.bind) {
7329        Function.prototype.bind = function (oThis) {
7330          if (typeof this !== "function") {
7331            // closest thing possible to the ECMAScript 5
7332            // internal IsCallable function
7333            throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
7334          }
7335
7336          var aArgs = Array.prototype.slice.call(arguments, 1), 
7337              fToBind = this, 
7338              fNOP = function () {},
7339              fBound = function () {
7340                return fToBind.apply(this instanceof fNOP && oThis
7341                       ? this
7342                       : oThis,
7343                       aArgs.concat(Array.prototype.slice.call(arguments)));
7344              };
7345
7346          fNOP.prototype = this.prototype;
7347          fBound.prototype = new fNOP();
7348
7349          return fBound;
7350        };
7351      }
7352
7353      // requestAnimationFrame polyfill by Erik M�ller. fixes from Paul Irish and Tino Zijdel
7354      // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
7355      // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
7356      // MIT license
7357      (function() {
7358        var lastTime = 0;
7359        var vendors = ['ms', 'moz', 'webkit', 'o'];
7360        for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
7361          window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
7362          window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
7363        }
7364        if (!window.requestAnimationFrame)
7365          window.requestAnimationFrame = function(callback, element) {
7366            var currTime = new Date().getTime();
7367            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
7368            var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);
7369            lastTime = currTime + timeToCall;
7370            return id;
7371          };
7372       
7373        if (!window.cancelAnimationFrame)
7374          window.cancelAnimationFrame = function(id) {
7375            clearTimeout(id);
7376          };
7377      }());
7378      
7379      // array.removeIf -> removes items from array base on a function's result
7380      Array.prototype.removeIf = function(callback) {
7381        var i = this.length;
7382        while (i--) {
7383          if (callback(this[i], i)) {
7384            this.splice(i, 1);
7385          }
7386        }
7387      };  
7388
7389      // IE11 for startsWith 
7390      // thanks to @lichtamberg - https://github.com/lichtamberg
7391      if (!String.prototype.startsWith) {
7392        String.prototype.startsWith = function(searchString, position) {
7393          position = position || 0;
7394          return this.indexOf(searchString, position) === position;
7395        };
7396      }
7397      
7398    }
7399    
7400    
7401    // Gallery clicked/touched -> retrieve & execute action
7402    function GalleryClicked(e) {
7403    
7404      var r = GalleryEventRetrieveElementl(e, false);
7405
7406      if( r.GOMidx == -1 ) { return 'exit'; }
7407      
7408      var idx = G.GOM.items[r.GOMidx].thumbnailIdx;
7409      if( G.GOM.slider.hostIdx == r.GOMidx ) {
7410        // slider on thumbnail -> open the displayed image
7411        idx = G.GOM.items[G.GOM.slider.currentIdx].thumbnailIdx;
7412      }
7413      switch( r.action ) {
7414        case 'OPEN':
7415          ThumbnailOpen(idx, false);
7416          return 'exit';
7417          break;
7418        case 'DISPLAY':
7419          // used the display icon (ignore if selection mode)
7420          ThumbnailOpen(idx, true);
7421          return 'exit';
7422          break;
7423        case 'TOGGLESELECT':
7424          ThumbnailSelectionToggle(idx);
7425          return 'exit';
7426          break;
7427        case 'SHARE':
7428          PopupShare(idx);
7429          return 'exit';
7430          break;
7431        case 'DOWNLOAD':
7432          DownloadImage(idx);
7433          return 'exit';
7434          break;
7435        case 'INFO':
7436          ItemDisplayInfo(G.I[idx]);
7437          return 'exit';
7438          break;
7439        case 'CART':
7440          AddToCart(idx);
7441          return 'exit';
7442          break;
7443        default:
7444          // all other actions (custom1..10, or anything else)
7445          var fu = G.O.fnThumbnailToolCustAction;
7446          if( fu !== null ) {
7447            typeof fu == 'function' ? fu(r.action, G.I[idx]) : window[fu](r.action, G.I[idx]);
7448          }
7449          break;
7450      }
7451    }
7452
7453    // Download an image
7454    function DownloadImage(idx) {
7455      if( G.I[idx].mediaKind != 'img' ) { return; }
7456
7457      
7458      var url = G.I[idx].src;
7459
7460      if( G.I[idx].downloadURL != undefined && G.I[idx].downloadURL != '' ) {
7461        url = G.I[idx].downloadURL;
7462      }
7463      
7464      var a = document.createElement('a');
7465      a.href = url;
7466      // a.download = url.split('.').pop();
7467      a.download = url.split('/').pop();
7468      a.target = '_blank';
7469      a.style.display = 'none';
7470      document.body.appendChild(a);
7471      a.click();
7472      document.body.removeChild(a);      
7473      
7474    }
7475    
7476    // add one image to the shopping cart
7477    function AddToCart( idx ) {
7478      // increment counter if already in shopping cart
7479      var found=false;
7480      for( var i=0; i<G.shoppingCart.length; i++ ) {
7481        if( G.shoppingCart[i].idx == idx ) {
7482          G.shoppingCart[i].cnt++;
7483          var fu = G.O.fnShoppingCartUpdated;
7484          if( fu !== null ) {
7485            typeof fu == 'function' ? fu(G.shoppingCart, G.I[idx]) : window[fu](G.shoppingCart, G.I[idx]);
7486          }
7487          TriggerCustomEvent('shoppingCartUpdated');
7488          return;
7489        }
7490      }
7491      
7492      // add to shopping cart
7493      if( !found) {
7494        G.shoppingCart.push( { idx:idx, ID:G.I[idx].GetID(), cnt:1} );
7495        var fu=G.O.fnShoppingCartUpdated;
7496        if( fu !== null ) {
7497          typeof fu == 'function' ? fu(G.shoppingCart, G.I[idx]) : window[fu](G.shoppingCart, G.I[idx]);
7498        }
7499        TriggerCustomEvent('shoppingCartUpdated');
7500      }
7501    }
7502    
7503
7504    // All thumbnails are set to unselected
7505    function ThumbnailSelectionClear() {
7506      G.GOM.nbSelected = 0;
7507      for( var i = 0, nbTn = G.GOM.items.length; i < nbTn ; i++ ) {
7508        var item = G .I[G.GOM.items[i].thumbnailIdx];
7509        if( item.selected ) {
7510          item.selected = false;
7511          var fu = G.O.fnThumbnailSelection;
7512          if( fu !== null ) {
7513            typeof fu == 'function' ? fu(item.$elt, item, G.I) : window[fu](item.$elt, item, G.I);
7514          }
7515        }
7516        item.selected = false;
7517      }
7518    }
7519    
7520    function ThumbnailSelectionToggle( idx ){
7521      var item = G.I[idx];
7522      if( item.selected === true ) {
7523        ThumbnailSelectionSet(item, false);
7524        G.GOM.nbSelected--;
7525        TriggerCustomEvent('itemUnSelected');
7526      }
7527      else {
7528        ThumbnailSelectionSet(item, true);
7529        G.GOM.nbSelected++;
7530        TriggerCustomEvent('itemSelected');
7531      }
7532    }
7533    
7534    
7535    // this replaces ThumbnailSelection()
7536    function ThumbnailSelectionSet(item, selected ){
7537      
7538      item.selected = selected;
7539      
7540      ThumbnailSelectionSetIcon( item );
7541      
7542      // called when the selection status of an item changed
7543      var fu=G.O.fnThumbnailSelection;
7544      if( fu !== null ) {
7545        typeof fu == 'function' ? fu(item.$elt, item, G.I) : window[fu](item.$elt, item, G.I);
7546      }
7547    
7548    }
7549    
7550    function ThumbnailSelectionSetIcon( item ) {
7551      if( item.$elt == null ) {
7552        // thumbnail is not built
7553        return;
7554      }
7555      // var $sub = item.$getElt('.nGY2GThumbnailSub');
7556      var $sub = item.$getElt('.nGY2GThumbnail');
7557      var $icon = item.$getElt('.nGY2GThumbnailIconImageSelect');
7558      if( item.selected === true) {
7559        $sub.addClass('nGY2GThumbnailSubSelected');
7560        $icon.addClass('nGY2ThumbnailSelected');
7561        $icon.removeClass('nGY2ThumbnailUnselected');
7562        $icon.html(G.O.icons.thumbnailSelected);
7563      }
7564      else {
7565        $sub.removeClass('nGY2GThumbnailSubSelected');
7566        $icon.removeClass('nGY2ThumbnailSelected');
7567        $icon.addClass('nGY2ThumbnailUnselected');
7568        $icon.html(G.O.icons.thumbnailUnselected);
7569      }
7570    }
7571    
7572    
7573    // display a modal popup for sharing image/album
7574    function PopupShare(idx) {
7575    
7576      // SEE SAMPLES: https://gist.github.com/chrisjlee/5196139
7577      // https://github.com/Julienh/Sharrre
7578    
7579      var item=G.I[idx];
7580
7581      var currentURL=document.location.protocol + '//' + document.location.hostname + document.location.pathname;
7582      var newLocationHash = '#nanogallery/' + G.baseEltID + '/';
7583      if( item.kind == 'image' ) {
7584        newLocationHash += item.albumID + '/' + item.GetID();
7585      }
7586      else {
7587        newLocationHash += item.GetID();
7588      }
7589    
7590      var content = '';
7591      content += '<div class="nGY2PopupOneItem" style="text-align:center;" data-share="facebook">'   + G.O.icons.shareFacebook +   '</div>';
7592      content += '<div class="nGY2PopupOneItem" style="text-align:center;" data-share="pinterest">'  + G.O.icons.sharePinterest +  '</div>';
7593      content += '<div class="nGY2PopupOneItem" style="text-align:center;" data-share="tumblr">'     + G.O.icons.shareTumblr +     '</div>';
7594      content += '<div class="nGY2PopupOneItem" style="text-align:center;" data-share="twitter">'    + G.O.icons.shareTwitter +    '</div>';
7595      // content += '<div class="nGY2PopupOneItem" style="text-align:center;" data-share="googleplus">' + G.O.icons.shareGooglePlus + '</div>';
7596      content += '<div class="nGY2PopupOneItem" style="text-align:center;" data-share="vk">'         + G.O.icons.shareVK +         '</div>';
7597      content += '<div class="nGY2PopupOneItem" style="text-align:center;" data-share="mail">'       + G.O.icons.shareMail +       '</div>';
7598      content += '<div class="nGY2PopupOneItem" style="text-align:center;"></div>';
7599      content += '<input class="nGY2PopupOneItemText" readonly type="text" value="' + currentURL+newLocationHash + '" style="width:100%;text-align:center;">';
7600      content += '<br>';
7601
7602      currentURL = encodeURIComponent(document.location.protocol + '//' + document.location.hostname + document.location.pathname + newLocationHash);
7603
7604      var currentTitle = item.title;
7605      var currentTn = item.thumbImg().src;
7606      
7607      
7608      Popup('Share to:', content, 'Center');
7609      
7610      G.popup.$elt.find('.nGY2PopupOneItem').on('click', function(e) {
7611        e.stopPropagation();
7612        
7613        var shareURL = '';
7614        var found = true;
7615        switch(jQuery(this).attr('data-share').toUpperCase()) {
7616          case 'FACEBOOK':
7617            // <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>
7618            //window.open("https://www.facebook.com/sharer.php?u="+currentURL,"","height=368,width=600,left=100,top=100,menubar=0");
7619            shareURL = 'https://www.facebook.com/sharer.php?u=' + currentURL;
7620            break;
7621          case 'VK':
7622            shareURL = 'http://vk.com/share.php?url=' + currentURL;
7623            break;
7624          case 'GOOGLEPLUS':
7625            shareURL = "https://plus.google.com/share?url=" + currentURL;
7626            break;
7627          case 'TWITTER':
7628            // shareURL="https://twitter.com/share?url="+currentURL+"&text="+currentTitle;
7629            shareURL = 'https://twitter.com/intent/tweet?text=' + currentTitle + 'url=' + currentURL;
7630            break;
7631          case 'PINTEREST':
7632            // shareURL='https://pinterest.com/pin/create/bookmarklet/?media='+currentTn+'&url='+currentURL+'&description='+currentTitle;
7633            shareURL = 'https://pinterest.com/pin/create/button/?media=' + currentTn + '&url=' + currentURL + '&description=' + currentTitle;
7634            break;
7635          case 'TUMBLR':
7636            //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;
7637            shareURL = 'http://www.tumblr.com/share/link?url=' + currentURL + '&name=' + currentTitle;
7638            break;
7639          case 'MAIL':
7640            shareURL = 'mailto:?subject=' + currentTitle + '&body=' + currentURL;
7641            break;
7642          default:
7643            found = false;
7644            break;
7645        }
7646        
7647        if( found ) {
7648          window.open(shareURL, "" , "height=550,width=500,left=100,top=100,menubar=0" );
7649          G.popup.close();
7650          // $popup.remove();
7651        }
7652        
7653      });
7654    }
7655    
7656    // build a modal popup
7657    function Popup(title, content, align) {
7658      var pp =  '<div class="nGY2Popup" style="opacity:0;"><div class="nGY2PopupContent' + align + '">';
7659      pp    +=  '<div class="nGY2PopupCloseButton">' + G.O.icons.buttonClose + '</div>';
7660      pp    +=  '<div class="nGY2PopupTitle">' + title + '</div>';
7661      pp    +=  content;
7662      pp    +=  '</div></div>';
7663      
7664      G.popup.$elt = jQuery(pp).appendTo('body');
7665      setElementOnTop( G.VOM.$viewer, G.popup.$elt);
7666      
7667      G.popup.isDisplayed = true;
7668      
7669      var tweenable = new NGTweenable();
7670      tweenable.tween({
7671        from:       { opacity: 0 },
7672        to:         { opacity: 1 },
7673        easing:     'easeInOutSine',
7674        duration:   250,
7675        step:       function (state, att) {
7676          G.popup.$elt.css( state );
7677        }
7678      });
7679      
7680      G.popup.$elt.find('.nGY2PopupCloseButton').on('click', function(e) {
7681        e.stopPropagation();
7682        G.popup.close();
7683      });
7684      
7685    }
7686
7687
7688    function GalleryMouseEnter(e) {
7689      if( !G.VOM.viewerDisplayed && G.GOM.albumIdx != -1 ) {
7690        var r = GalleryEventRetrieveElementl(e, true);
7691        // if( r.action == 'OPEN' && r.GOMidx != -1 ) {
7692        if( r.GOMidx != -1 ) {
7693          var target = e.target || e.srcElement;
7694          // if( target.getAttribute('class') != 'nGY2GThumbnail' ) { return; }
7695          ThumbnailHover(r.GOMidx);
7696        }
7697      }
7698    }
7699    
7700    function GalleryMouseLeave(e) {
7701      if( !G.VOM.viewerDisplayed && G.GOM.albumIdx != -1 ) {
7702        var r = GalleryEventRetrieveElementl(e, true);
7703        if( r.GOMidx != -1 ) {
7704          var target = e.target || e.srcElement;
7705          // if( target.getAttribute('class') != 'nGY2GThumbnail' ) { return; }
7706          ThumbnailHoverOut(r.GOMidx);
7707        }
7708      }
7709    }
7710    
7711    function GalleryEventRetrieveElementl( e, ignoreSubItems ) {
7712      var r = { action: 'NONE', GOMidx: -1 };
7713      
7714      if( e == undefined ) {
7715        return r;
7716      }
7717      var target = e.target || e.srcElement;
7718      while( target != G.$E.conTnParent[0] ) {       // loop element parent up to find the thumbnail element
7719        if( jQuery(target).hasClass('nGY2GThumbnail') ) {
7720          if( r.action == 'NONE' ) {
7721            r.action = 'OPEN';
7722          }
7723          r.GOMidx = jQuery(target).data('index');
7724          return r;
7725        }
7726        // if( !ignoreSubItems && jQuery(target).hasClass('nGY2GThumbnailIcon') ) {
7727        if( !ignoreSubItems ) {
7728          var a = jQuery(target).data('ngy2action');
7729          if( a != '' && a != undefined ) {
7730            r.action = a;
7731          }
7732        }
7733        if( target.parentNode == null ) {
7734          return r;
7735        }
7736        target = target.parentNode;
7737      }
7738      return r;
7739    }
7740    
7741
7742    // Open one thumbnail
7743    function ThumbnailOpen( idx, ignoreSelected ) {
7744      var item = G.I[idx];
7745
7746      var fu = G.O.fnThumbnailClicked;
7747      if( fu !== null ) {
7748        typeof fu == 'function' ? fu(item.$elt, item) : window[fu](item.$elt, item);
7749      }
7750      
7751      // open URL
7752      if( item.destinationURL !== undefined && item.destinationURL.length > 0 ) {
7753        window.location = item.destinationURL;
7754        return;
7755      }
7756
7757      switch( item.kind ) {
7758        case 'image':
7759          if( ignoreSelected === false && G.GOM.nbSelected > 0 ) {
7760            ThumbnailSelectionToggle(idx);
7761          }
7762          else {
7763            // display image
7764            DisplayPhotoIdx(idx);
7765          }
7766          break;
7767        case 'album':
7768          if( ignoreSelected === false && G.GOM.nbSelected > 0 ) {
7769            ThumbnailSelectionToggle(idx);
7770          }
7771          else {
7772            if( G.O.thumbnailAlbumDisplayImage && idx != 0 ) {
7773              // display album content in lightbox
7774              DisplayFirstMediaInAlbum(idx);
7775              return;
7776            }
7777            else {
7778              // display album content in gallery
7779              DisplayAlbum('-1', item.GetID());
7780            }
7781          }
7782          break;
7783        case 'albumUp':
7784          var parent = NGY2Item.Get(G, item.albumID);
7785          DisplayAlbum('-1', parent.albumID);
7786          break;
7787      }
7788    }
7789    
7790    function DisplayFirstMediaInAlbum( albumIdx ) {
7791      if( G.O.debugMode ) { console.log('#DisplayFirstPhotoInAlbum : '+  albumIdx); }
7792
7793      var item = G.I[albumIdx];
7794      
7795      var l = G.I.length;
7796      for( var i = 0; i < l; i++ ) {
7797        if( G.I[i].albumID == item.GetID() ) {
7798          DisplayPhotoIdx( i );
7799          return;
7800        }
7801      }
7802      
7803      // load album content
7804      AlbumGetContent( item.GetID(), DisplayFirstMediaInAlbum, albumIdx, null );
7805      
7806    }
7807    
7808
7809    // Open link to original image (new window)
7810    function OpenOriginal( item ) {
7811      switch( G.O.kind ) {
7812        case 'flickr':
7813          var sU = 'https://www.flickr.com/photos/' + G.O.userID + '/' + item.GetID();
7814          if( item.albumID != '0' ) {
7815            sU += '/in/album-' + item.albumID + '/';
7816          }
7817          window.open(sU, '_blank');
7818          break;
7819        case 'picasa':
7820        case 'google':
7821        case 'google2':
7822          // no more working since Google changed the access to Google Photos in 2017
7823          // var sU='https://plus.google.com/photos/'+G.O.userID+'/albums/'+item.albumID+'/'+item.GetID();
7824          // window.open(sU,'_blank');
7825          // break;
7826        default:
7827          var sU = item.responsiveURL();
7828          window.open(sU, '_blank');
7829          break;
7830      }
7831    }
7832    
7833    // Display one photo (with internal or external viewer)
7834    function DisplayPhotoIdx( ngy2ItemIdx ) {
7835
7836      if( !G.O.thumbnailOpenImage ) { return; }
7837
7838      if( G.O.thumbnailOpenOriginal ) {
7839        // Open link to original image
7840        OpenOriginal( G.I[ngy2ItemIdx] );
7841        return;
7842      }
7843        
7844      var items = [];
7845      G.VOM.currItemIdx = 0;
7846      G.VOM.items = [];
7847      G.VOM.albumID = G.I[ngy2ItemIdx].albumID;
7848      
7849      var vimg = new VImg(ngy2ItemIdx);
7850      G.VOM.items.push(vimg);
7851      items.push(G.I[ngy2ItemIdx]);
7852      //TODO -> danger? -> pourquoi reconstruire la liste si d�j� ouvert (back/forward)     
7853      var l = G.I.length;
7854      for( var idx = ngy2ItemIdx+1; idx < l ; idx++) {
7855        var item = G.I[idx];
7856        if( item.kind == 'image' && item.isToDisplay(G.VOM.albumID) && item.destinationURL == '' ) {
7857          var vimg = new VImg(idx);
7858          G.VOM.items.push(vimg);
7859          items.push(item);
7860        }
7861      }
7862      var last = G.VOM.items.length;
7863      var cnt = 1;
7864      for( var idx = 0; idx < ngy2ItemIdx ; idx++) {
7865        var item = G.I[idx];
7866        if( item.kind == 'image' && item.isToDisplay(G.VOM.albumID) && item.destinationURL == '' ) {
7867          var vimg = new VImg(idx);
7868          vimg.mediaNumber = cnt;
7869          G.VOM.items.push(vimg);
7870          items.push(item);
7871          cnt++;
7872        }
7873      }
7874      for( var i = 0; i < last; i++ ) {
7875        G.VOM.items[i].mediaNumber = cnt;
7876        cnt++;
7877      }
7878    
7879      // opens media with external viewer
7880      var fu = G.O.fnThumbnailOpen;
7881      if( fu !== null ) {
7882        typeof fu == 'function' ? fu(items) : window[fu](items);
7883        return;
7884      }
7885    
7886      // use internal viewer
7887      if( !G.VOM.viewerDisplayed ) {
7888        // build viewer and display
7889        OpenInternalViewer();
7890      }
7891      else {
7892        // viewer already displayed -> display new media in current viewer
7893        G.VOM.$mediaCurrent.empty();
7894        var item = G.VOM.NGY2Item(0);
7895        var spreloader = '<div class="nGY2ViewerMediaLoaderDisplayed"></div>';
7896        if( item.mediaKind == 'img' && item.imageWidth != 0 && item.imageHeight != 0 ) {
7897          spreloader = '<div class="nGY2ViewerMediaLoaderHidden"></div>';
7898        }
7899        G.VOM.$mediaCurrent.append( spreloader + item.mediaMarkup);
7900        ViewerSetMediaVisibility(G.VOM.NGY2Item(1), G.VOM.$mediaNext, 0);
7901        ViewerSetMediaVisibility(G.VOM.NGY2Item(-1), G.VOM.$mediaPrevious, 0);
7902        if( item.mediaKind == 'img' ) {
7903          G.VOM.ImageLoader.loadImage(VieweImgSizeRetrieved, item);
7904        }
7905        // G.VOM.$mediaCurrent.css({ opacity:0 }).attr('src','');
7906        // G.VOM.ImageLoader.loadImage(VieweImgSizeRetrieved, G.VOM.NGY2Item(0));
7907        // G.VOM.$mediaCurrent.children().eq(0).attr('src',G.emptyGif).attr('src', G.VOM.NGY2Item(0).responsiveURL());
7908        DisplayInternalViewer(0, '');
7909      }
7910    }
7911    
7912    function ViewerZoomStart() {
7913      if( G.O.viewerZoom && !G.VOM.viewerMediaIsChanged ) {
7914        var item=G.VOM.NGY2Item(0);
7915        if( item.imageHeight > 0 && item.imageWidth > 0 ) {
7916          if( G.VOM.zoom.isZooming === false ) {
7917            // default zoom
7918            G.VOM.zoom.userFactor = 1;
7919            G.VOM.zoom.isZooming = true;
7920          }
7921          return true;
7922        }
7923      }
7924    }
7925          
7926    function ViewerZoomIn( zoomIn ) {
7927    if( zoomIn ) {
7928        // zoom in
7929        G.VOM.zoom.userFactor+=0.1;
7930        ViewerZoomMax();
7931      }
7932      else {
7933        // zoom out
7934        G.VOM.zoom.userFactor-=0.1;
7935        ViewerZoomMin();
7936      }
7937      ViewerMediaSetPosAndZoom();
7938    }
7939    
7940    function ViewerZoomMax() {
7941      if( G.VOM.zoom.userFactor > 3 ) {
7942        G.VOM.zoom.userFactor = 3;
7943      }
7944    }
7945    function ViewerZoomMin() {
7946    
7947      if( G.VOM.zoom.userFactor < 0.2 ) {
7948        G.VOM.zoom.userFactor = 0.2;
7949      }
7950    }
7951    
7952    
7953    
7954    // Set position and size of all 3 media containers
7955    function ViewerMediaSetPosAndZoom() {
7956    
7957      if( !G.VOM.zoom.isZooming ) {
7958        G.VOM.zoom.userFactor = 1;
7959      }
7960      // window.ng_draf( function() {
7961        ViewerMediaSetPosAndZoomOne( G.VOM.NGY2Item(0), G.VOM.$mediaCurrent, true );
7962        ViewerMediaSetPosAndZoomOne( G.VOM.NGY2Item(-1), G.VOM.$mediaPrevious, false );
7963        ViewerMediaSetPosAndZoomOne( G.VOM.NGY2Item(1), G.VOM.$mediaNext, false );
7964      // });
7965    }
7966    
7967
7968    
7969    // Media which is not IMG -> center and set size
7970    function ViewerMediaCenterNotImg( $mediaContainer ) {
7971      var $media = $mediaContainer.children().eq(1);
7972      $media.css( {'height': '80%' });
7973      $media.css( {'width':  '90%' });
7974      $media[0].style[G.CSStransformName] = 'translate(0px, "50%") ';
7975    }
7976    
7977    // Set position and size of ONE media container
7978    function ViewerMediaSetPosAndZoomOne(item, $img, isCurrent ) {
7979
7980      if( item.mediaKind != 'img' ) {
7981        ViewerMediaCenterNotImg($img);
7982        return;
7983      }
7984      
7985      if( item.imageHeight == 0 || item.imageWidth == 0 ) { 
7986        ViewerSetMediaVisibility( item, $img, 0 );
7987        return;
7988      }
7989
7990      // part 1: set the image size
7991      var zoomUserFactor = isCurrent == true ? G.VOM.zoom.userFactor : 1;
7992      
7993      var dpr = 1;
7994      if( G.O.viewerImageDisplay == 'bestImageQuality' ) {
7995        dpr = window.devicePixelRatio;
7996      }
7997      
7998      // retrieve the base zoom factor (image fill screen)
7999      var zoomBaseFactorW = (G.VOM.window.lastWidth  - G.VOM.padding.V) / (item.imageWidth  / dpr);
8000      var zoomBaseFactorH = (G.VOM.window.lastHeight - G.VOM.padding.H) / (item.imageHeight / dpr);
8001      var zoomBaseFactor = Math.min(zoomBaseFactorW, zoomBaseFactorH);
8002      if( zoomBaseFactor > 1 && G.O.viewerImageDisplay != 'upscale' ) {
8003        // no upscale
8004        zoomBaseFactor = 1;
8005      }
8006
8007      var imageCurrentHeight = (item.imageHeight / dpr) * zoomUserFactor * zoomBaseFactor;
8008      var imageCurrentWidth  = (item.imageWidth / dpr)  * zoomUserFactor * zoomBaseFactor;
8009      $img.children().eq(1).css( {'height': imageCurrentHeight });
8010      $img.children().eq(1).css( {'width':  imageCurrentWidth  });
8011
8012      // retrieve posX/Y to center image
8013      var posX = 0;
8014      if( imageCurrentWidth > G.VOM.window.lastWidth ) {
8015        posX = -(imageCurrentWidth - G.VOM.window.lastWidth)/2;
8016      }
8017      var posY = 0;
8018      if( imageCurrentHeight > G.VOM.window.lastHeight ) {
8019        posY = ( imageCurrentHeight - G.VOM.window.lastHeight ) / 2;
8020      }
8021      posY = 0;   // actually, it seems that the image is always centered vertically -> so no need to to anything
8022      
8023      // Part 2: set the X/Y position (for zoom/pan)
8024      if( isCurrent ) {
8025        if( !G.VOM.zoom.isZooming ) {
8026          G.VOM.panPosX = 0;
8027          G.VOM.panPosY = 0;
8028        }
8029        G.VOM.zoom.posX = posX;
8030        G.VOM.zoom.posY = posY;
8031        ViewerImagePanSetPosition(G.VOM.panPosX, G.VOM.panPosY, $img, false);
8032      }
8033      // else {
8034        //$img[0].style[G.CSStransformName]= 'translate3D('+ posX+'px, '+ posY+'px, 0) ';
8035      // }
8036      else {
8037        // set the pan position of each media container
8038        ViewerMediaPanX( G.VOM.swipePosX );
8039        $img.children().eq(1)[0].style[G.CSStransformName]= 'translate(0px, 0px) rotate('+ item.rotationAngle +'deg)';
8040      }
8041      
8042    }
8043
8044    // position the image depending on the zoom factor and the pan X/Y position
8045    // IMG is the only media supporting zoom/pan
8046    function ViewerImagePanSetPosition(posX, posY, imageContainer, savePosition ) {
8047      if( savePosition ) {
8048        G.VOM.panPosX = posX;
8049        G.VOM.panPosY = posY;
8050      }
8051
8052      posX += G.VOM.zoom.posX;
8053      posY += G.VOM.zoom.posY;
8054      
8055      // imageContainer.children().eq(1)[0].style[G.CSStransformName]= 'translate('+ posX + 'px, '+ posY + 'px)';
8056      imageContainer.children().eq(1)[0].style[G.CSStransformName]= 'translate('+ posX + 'px, '+ posY + 'px) rotate('+ G.VOM.NGY2Item(0).rotationAngle +'deg)';
8057
8058
8059    }
8060    
8061
8062    // display media with internal viewer
8063    function OpenInternalViewer(  ) {
8064
8065      G.VOM.viewerDisplayed = true;
8066      G.GOM.firstDisplay = false;
8067      
8068      G.VOM.saveOverflowX = window.getComputedStyle(document.body)['overflow-x'];
8069      G.VOM.saveOverflowY = window.getComputedStyle(document.body)['overflow-y'];
8070      jQuery('body').css({ overflow: 'hidden' });  //avoid scrollbars
8071
8072      G.VOM.$cont = jQuery('<div  class="nGY2 nGY2ViewerContainer" style="opacity:1"></div>').appendTo('body');
8073      
8074      SetViewerTheme();
8075
8076      G.VOM.$viewer = jQuery('<div class="nGY2Viewer" style="opacity:0" itemscope itemtype="http://schema.org/ImageObject"></div>').appendTo( G.VOM.$cont );
8077      G.VOM.$viewer.css({ msTouchAction: 'none', touchAction: 'none' });            // avoid pinch zoom
8078
8079      G.VOM.currItemIdx = 0;
8080      var sMedia = '<div class="nGY2ViewerMediaPan"><div class="nGY2ViewerMediaLoaderDisplayed"></div>' + G.VOM.NGY2Item(-1).mediaMarkup + '</div>';    // previous media
8081      sMedia    += '<div class="nGY2ViewerMediaPan"><div class="nGY2ViewerMediaLoaderDisplayed"></div>' + G.VOM.NGY2Item(0).mediaMarkup  + '</div>';    // current media
8082      sMedia    += '<div class="nGY2ViewerMediaPan"><div class="nGY2ViewerMediaLoaderDisplayed"></div>' + G.VOM.NGY2Item(1).mediaMarkup  + '</div>';    // next media
8083
8084      var sNav = '';
8085      var iconP = G.O.icons.viewerImgPrevious;
8086      if( iconP != undefined && iconP != '') {
8087        sNav += '<div class="nGY2ViewerAreaPrevious ngy2viewerToolAction" data-ngy2action="previous">' + iconP + '</div>';
8088      }
8089      var iconN = G.O.icons.viewerImgNext;
8090      if( iconN != undefined && iconN != '') {
8091        sNav += '<div class="nGY2ViewerAreaNext ngy2viewerToolAction" data-ngy2action="next">' + iconN + '</div>';
8092      }
8093
8094      G.VOM.$content = jQuery('<div class="nGY2ViewerContent">' + sMedia + sNav + '</div>').appendTo( G.VOM.$viewer );
8095
8096      var $mediaPan = G.VOM.$content.find('.nGY2ViewerMediaPan');
8097      G.VOM.$mediaPrevious = $mediaPan.eq(0);    // pointer to previous media container
8098      G.VOM.$mediaCurrent = $mediaPan.eq(1);     // pointer to current media container
8099      G.VOM.$mediaNext = $mediaPan.eq(2);        // pointer to next media container
8100
8101      G.VOM.ImageLoader.loadImage( VieweImgSizeRetrieved, G.VOM.NGY2Item(0)  );
8102      G.VOM.ImageLoader.loadImage( VieweImgSizeRetrieved, G.VOM.NGY2Item(-1) );
8103      G.VOM.ImageLoader.loadImage( VieweImgSizeRetrieved, G.VOM.NGY2Item(1)  );
8104      
8105      G.VOM.padding.H = parseInt(G.VOM.$content.css("padding-left")) + parseInt(G.VOM.$content.css("padding-right"));
8106      G.VOM.padding.V = parseInt(G.VOM.$content.css("padding-top")) + parseInt(G.VOM.$content.css("padding-bottom"));
8107      
8108      // build media toolbar container
8109      var vtbBg1 = '';
8110      var vtbBg2 = ' toolbarBackground';
8111      if( G.O.viewerToolbar.fullWidth ) {
8112        vtbBg1 = ' toolbarBackground';
8113        vtbBg2 = '';
8114      }
8115      var vtbAlign = 'text-align:center;';
8116      switch ( G.O.viewerToolbar.align ) {
8117        case 'left':
8118          vtbAlign = 'text-align:left;';
8119          break;
8120        case 'right':
8121          vtbAlign = 'text-align:right;';
8122          break;
8123      }
8124      var sTB = '<div class="toolbarContainer nGEvent' + vtbBg1 + '" style="visibility:' +(G.O.viewerToolbar.display ? "visible" : "hidden")+';'+vtbAlign+'"><div class="toolbar nGEvent' + vtbBg2 + '"></div></div>';
8125      G.VOM.$toolbar = jQuery(sTB).appendTo(G.VOM.$viewer);
8126
8127      if( G.VOM.toolbarMode == 'min' || (G.O.viewerToolbar.autoMinimize > 0 && G.O.viewerToolbar.autoMinimize >= G.GOM.cache.viewport.w) ) {
8128        ViewerToolbarForVisibilityMin();
8129      }
8130      else {
8131        ViewerToolbarForVisibilityStd();
8132      }
8133      
8134      // top-left toolbar
8135      if( G.O.viewerTools.topLeft != '' ) {
8136        var sTopLeft = '<div class="nGY2ViewerToolsTopLeft nGEvent"><div class="toolbar nGEvent">';
8137        var sTL = G.O.viewerTools.topLeft.split(',');
8138        for( var i = 0, sTLL = sTL.length; i < sTLL; i++) {
8139          sTopLeft += ToolbarAddElt( sTL[i] );
8140        }
8141        sTopLeft += '</div></div>';
8142        G.VOM.$toolbarTL = jQuery(sTopLeft).appendTo(G.VOM.$viewer);
8143      }
8144      // top-right toolbar
8145      if( G.O.viewerTools.topRight != '' ) {
8146        var sTopRight = '<div class="nGY2ViewerToolsTopRight nGEvent"><div class="toolbar nGEvent">';
8147        var sTR = G.O.viewerTools.topRight.split(',');
8148        for( var i = 0, sTRL = sTR.length; i < sTRL; i++) {
8149          sTopRight += ToolbarAddElt( sTR[i] );
8150        }
8151        sTopRight += '</div></div>';
8152        G.VOM.$toolbarTR = jQuery(sTopRight).appendTo(G.VOM.$viewer);
8153      }
8154
8155      // set the events handler for toolbars
8156      ViewerToolsOn();
8157
8158      // display logo
8159      if( G.O.viewerDisplayLogo ) { G.$E.vwLogo=jQuery('<div class="nGY2 nGY2ViewerLogo"></div>').appendTo(G.VOM.$viewer); }
8160
8161      // Go to fullscreen mode
8162      if( ngscreenfull.enabled && G.O.viewerFullscreen ) { ngscreenfull.request(); }
8163
8164      setElementOnTop('', G.VOM.$viewer);
8165      ResizeInternalViewer(true);
8166      G.VOM.timeImgChanged = new Date().getTime();
8167      
8168      // viewer display transition
8169      var tweenable = new NGTweenable();
8170      tweenable.tween({
8171        from:         { opacity: 0, posY: G.VOM.window.lastHeight*.5 },
8172        to:           { opacity: 1, posY: 0 },
8173        delay:        30,
8174        duration:     500,
8175        easing:       'easeOutQuart',
8176        step:         function (state) {
8177          G.VOM.$viewer.css('opacity', state.opacity);
8178          G.VOM.$viewer[0].style[G.CSStransformName] = 'translateY(' + (state.posY) + 'px) ';
8179        }
8180      });
8181
8182      // stop click propagation on media ==> if the user clicks outside of an media, the viewer is closed
8183      // --> no more supported since v2.0.0
8184      // G.VOM.$viewer.find('img').on('click', function (e) { e.stopPropagation(); });
8185      
8186      
8187      ViewerMediaPanX(0);
8188      ViewerSetEvents();
8189
8190      DisplayInternalViewer(0, '');
8191      
8192      if( G.O.slideshowAutoStart ) {
8193        G.VOM.playSlideshow = false;
8194        SlideshowToggle();
8195      }
8196    }
8197    
8198    function ViewerEvents() {
8199      if( !G.VOM.viewerDisplayed || G.VOM.viewerMediaIsChanged || G.VOM.NGY2Item(0).mediaKind != 'img') {
8200        // ignore fired event if viewer not displayed or if currently changed or if current media not an image
8201        return false;
8202      }
8203      return true;
8204    }
8205    
8206    
8207    // viewer gesture handling
8208    function ViewerSetEvents() {
8209
8210      if( G.VOM.hammertime == null ) {
8211      
8212        G.VOM.hammertime =  new NGHammer.Manager(G.VOM.$cont[0], {
8213          recognizers: [
8214            [NGHammer.Pinch, { enable: true }],
8215            [NGHammer.Pan, { direction: NGHammer.DIRECTION_ALL }]
8216          ]
8217        });
8218     
8219        // PAN
8220        G.VOM.hammertime.on('pan', function(ev) {
8221          if( !ViewerEvents() ) { return; }
8222
8223          if( G.VOM.zoom.isZooming ) {
8224            // pan zoomed image
8225            ViewerImagePanSetPosition(G.VOM.panPosX + ev.deltaX, G.VOM.panPosY + ev.deltaY, G.VOM.$mediaCurrent, false);
8226            if( G.VOM.toolbarsDisplayed == true ) {
8227              G.VOM.toolsHide();
8228            }
8229          }
8230          else {
8231            if( ev.deltaY > G.VOM.panThreshold && Math.abs(ev.deltaX) < G.VOM.panThreshold && !G.VOM.panXOnly ) {
8232              // pan viewer down
8233              ViewerMediaPanX( 0 );
8234              var dist = Math.min(ev.deltaY, 200);
8235              G.VOM.$viewer[0].style[G.CSStransformName] = 'translateY(' + dist + 'px) ';
8236              G.VOM.$viewer.css('opacity', 1-dist/200/2);
8237            }
8238            else {
8239              // pan media left/right
8240              if( Math.abs(ev.deltaX) > G.VOM.panThreshold ) {
8241                G.VOM.panXOnly = true;
8242              }
8243              ViewerMediaPanX( ev.deltaX );
8244              G.VOM.$viewer[0].style[G.CSStransformName] = 'translateY(0px)';
8245              G.VOM.$viewer.css('opacity', 1);
8246            }
8247          }
8248        });
8249
8250        // PAN END
8251        G.VOM.hammertime.on('panend', function(ev) {
8252          if( !ViewerEvents() ) { return; }
8253          if( G.VOM.zoom.isZooming ) {
8254            G.VOM.timeImgChanged = new Date().getTime();
8255            ViewerImagePanSetPosition(G.VOM.panPosX+ev.deltaX, G.VOM.panPosY+ev.deltaY, G.VOM.$mediaCurrent, true);
8256          }
8257          else {
8258            var panY = false;
8259            if( !G.VOM.panXOnly ) {
8260              if( ev.deltaY > 50 && Math.abs(ev.deltaX) < 50 ) {
8261                // close viewer
8262                CloseInternalViewer(G.VOM.currItemIdx);
8263                panY = true;
8264              }
8265            }
8266            if( !panY ) {
8267              if( Math.abs( ev.deltaX ) < 50 ) {
8268                ViewerMediaPanX(0);
8269              }
8270              else {
8271                ev.deltaX > 50 ? DisplayPreviousMedia() : DisplayNextMedia();
8272              }
8273            }
8274          }
8275          G.VOM.panXOnly = false;
8276        });
8277        
8278        if( G.O.viewerZoom ) {
8279        
8280          G.VOM.hammertime.add( new NGHammer.Tap({ event: 'doubletap', taps: 2 }) );
8281          G.VOM.hammertime.add( new NGHammer.Tap({ event: 'singletap' }) );
8282          G.VOM.hammertime.get('doubletap').recognizeWith('singletap');
8283          G.VOM.hammertime.get('singletap').requireFailure('doubletap');
8284
8285          // single tap -> next/previous media
8286          G.VOM.hammertime.on('singletap', function(ev) {
8287            if( !ViewerEvents() ) { return; }
8288            StopPropagationPreventDefault(ev.srcEvent);
8289            if( G.VOM.toolbarsDisplayed == false ) {
8290              debounce( ViewerToolsUnHide, 100, false)();
8291            }
8292            else {
8293              if( ev.target.className.indexOf('nGY2ViewerMedia') !== -1 ) {
8294                if( ev.srcEvent.pageX < (G.GOM.cache.viewport.w/2) ) {
8295                  DisplayPreviousMedia();
8296                }
8297                else {
8298                  DisplayNextMedia();
8299                }
8300              }
8301            }
8302          });
8303          
8304          // double tap -> zoom
8305          G.VOM.hammertime.on('doubletap', function(ev) {
8306            if( !ViewerEvents() ) { return; }
8307            StopPropagationPreventDefault(ev.srcEvent);
8308            
8309            if( ev.target.className.indexOf('nGY2ViewerMedia') !== -1 ) {
8310              // double tap only on image
8311              if( G.VOM.zoom.isZooming ) {
8312                G.VOM.zoom.isZooming = false;
8313                // G.VOM.zoom.userFactor = 1;
8314                ResizeInternalViewer(true);
8315              }
8316              else {
8317                if( ViewerZoomStart() ) {
8318                  G.VOM.zoom.userFactor = 1.5;
8319                  ViewerMediaSetPosAndZoom();
8320                }
8321              }
8322            }
8323          });
8324        
8325          // pinch end
8326          G.VOM.hammertime.on('pinchend', function(ev) {
8327            ev.srcEvent.stopPropagation();
8328            ev.srcEvent.preventDefault();  // cancel  mouseenter event
8329            G.VOM.timeImgChanged = new Date().getTime();
8330          });
8331          G.VOM.hammertime.on('pinch', function(ev) {
8332            ev.srcEvent.stopPropagation();
8333            ev.srcEvent.preventDefault();  // cancel  mouseenter event
8334            
8335            if( ViewerZoomStart() ) {
8336              G.VOM.zoom.userFactor = ev.scale;
8337              ViewerZoomMax();
8338              ViewerZoomMin();
8339              ViewerMediaSetPosAndZoom();   // center media
8340            }
8341          });
8342        }
8343        else {
8344          // no zoom -> click/tap on image to go to next/previous one
8345          G.VOM.hammertime.on('tap', function(ev) {
8346            if( !ViewerEvents() ) { return; }
8347            StopPropagationPreventDefault(ev.srcEvent);
8348            if( G.VOM.toolbarsDisplayed == false  ){
8349              // display tools on tap if hidden
8350              debounce( ViewerToolsUnHide, 100, false)();
8351            }
8352            else {
8353              // display next/previous image if tools not hidden
8354              if( ev.target.className.indexOf('nGY2ViewerMedia') !== -1 ) {
8355                if( ev.srcEvent.pageX < (G.GOM.cache.viewport.w/2) ) {
8356                  DisplayPreviousMedia();
8357                }
8358                else {
8359                  DisplayNextMedia();
8360                }
8361              }
8362            }
8363            
8364          });
8365        }
8366      }
8367    }
8368    
8369    function StopPropagationPreventDefault(e) {
8370      e.stopPropagation();
8371      e.preventDefault();
8372    }
8373
8374    // Hide toolbars on user inactivity
8375    function ViewerToolsHide() {
8376      if( G.VOM.viewerDisplayed ) {
8377        G.VOM.toolbarsDisplayed = false;
8378        ViewerToolsOpacity(0);
8379      }
8380    }
8381    
8382    function ViewerToolsUnHide() {
8383    if( G.VOM.viewerDisplayed ) {
8384        G.VOM.toolbarsDisplayed = true;
8385        ViewerToolsOpacity(1);
8386        G.VOM.toolsHide();
8387      }
8388    }
8389    
8390    function ViewerToolsOpacity( op ) {
8391      if( G.VOM.$toolbar != null ) {
8392        G.VOM.$toolbar.css('opacity', op);
8393      }
8394      if( G.VOM.$toolbarTL != null ) {
8395        G.VOM.$toolbarTL.css('opacity', op);
8396      }
8397      if( G.VOM.$toolbarTR != null ) { 
8398        G.VOM.$toolbarTR.css('opacity', op);
8399      }
8400      G.VOM.$content.find('.nGY2ViewerAreaNext').css('opacity', op);
8401      G.VOM.$content.find('.nGY2ViewerAreaPrevious').css('opacity', op);
8402    }
8403    
8404    
8405    
8406    function ViewerToolsOn() {
8407      // removes all events
8408      G.VOM.$viewer.off('touchstart click', '.ngy2viewerToolAction', ViewerToolsAction); 
8409      
8410      // action button
8411      G.VOM.$viewer.on('touchstart click', '.ngy2viewerToolAction', ViewerToolsAction); 
8412    }
8413
8414    
8415    // Actions of the buttton/elements
8416    function ViewerToolsAction(e) {
8417      // delay to avoid twice handling on smartphone/tablet (both touchstart click events are fired)
8418      if( (new Date().getTime()) - G.timeLastTouchStart < 300 ) { return; }
8419      G.timeLastTouchStart = new Date().getTime();
8420      
8421      var $this = $(this);
8422      var ngy2action = $this.data('ngy2action');
8423      if( ngy2action == undefined ) { return; }
8424      switch( ngy2action ) {
8425        case 'next':
8426          StopPropagationPreventDefault(e);
8427          DisplayNextMedia();
8428          break;
8429        case 'previous':
8430          StopPropagationPreventDefault(e);
8431          DisplayPreviousMedia();
8432          break;
8433        case 'playPause':
8434          e.stopPropagation();
8435          SlideshowToggle();
8436          break;
8437        case 'zoomIn':
8438          StopPropagationPreventDefault(e);
8439          if( ViewerZoomStart() ) { ViewerZoomIn( true ); }
8440          break;
8441        case 'zoomOut':
8442          StopPropagationPreventDefault(e);
8443          if( ViewerZoomStart() ) { ViewerZoomIn( false ); }
8444          break;
8445        case 'minimize':
8446          // toggle toolbar visibility
8447          StopPropagationPreventDefault(e);
8448          if( G.VOM.toolbarMode == 'std' ) {
8449            ViewerToolbarForVisibilityMin();
8450          }
8451          else {
8452            ViewerToolbarForVisibilityStd();
8453          }
8454          break;
8455        case 'fullScreen':
8456          // Toggle viewer fullscreen mode on/off
8457          e.stopPropagation();
8458          if( ngscreenfull.enabled ) {
8459            ngscreenfull.toggle();
8460          }
8461          break;
8462        case 'info':
8463          e.stopPropagation();
8464          ItemDisplayInfo(G.VOM.NGY2Item(0));
8465          break;
8466        case 'close':
8467          StopPropagationPreventDefault(e);
8468          if( (new Date().getTime()) - G.VOM.timeImgChanged < 400 ) { return; }
8469          CloseInternalViewer(G.VOM.currItemIdx);
8470          break;
8471        case 'download':
8472          StopPropagationPreventDefault(e);
8473          DownloadImage(G.VOM.items[G.VOM.currItemIdx].ngy2ItemIdx);
8474          break;
8475        case 'share':
8476          StopPropagationPreventDefault(e);
8477          PopupShare(G.VOM.items[G.VOM.currItemIdx].ngy2ItemIdx);
8478          break;
8479        case 'linkOriginal':
8480          StopPropagationPreventDefault(e);
8481          OpenOriginal( G.VOM.NGY2Item(0) );
8482          break;
8483        case 'rotateLeft':
8484          StopPropagationPreventDefault(e);
8485          ViewerImageRotate(-90);
8486          break;
8487        case 'rotateRight':
8488          StopPropagationPreventDefault(e);
8489          ViewerImageRotate(90);
8490          break;
8491      }
8492      
8493      // custom button
8494      var fu = G.O.fnImgToolbarCustClick;
8495      if( ngy2action.indexOf('custom') == 0  && fu !== null ) {
8496        typeof fu == 'function' ? fu(ngy2action, $this, G.VOM.NGY2Item(0)) : window[fu](ngy2action, $this, G.VOM.NGY2Item(0));
8497      }
8498    }
8499
8500    // rotate displayed image
8501    function ViewerImageRotate( angle ) {
8502      var item = G.VOM.NGY2Item(0);
8503      if( item.mediaKind == 'img' ) {
8504        item.rotationAngle += angle;
8505        item.rotationAngle = item.rotationAngle % 360;
8506        if( item.rotationAngle < 0 ) {
8507          item.rotationAngle += 360;
8508        }
8509        ViewerMediaPanX( 0 );
8510        ViewerMediaSetPosAndZoomOne( G.VOM.NGY2Item(0), G.VOM.$mediaCurrent, true );
8511      }
8512    }
8513     
8514
8515    // Display photo infos
8516    function ItemDisplayInfo( item ) {
8517
8518      var content = '<div class="nGY2PopupOneItemText">' + item.title + '</div>';
8519      content    += '<div class="nGY2PopupOneItemText">' + item.description + '</div>';
8520      if( item.author != '' ) {
8521        content  += '<div class="nGY2PopupOneItemText">' + G.O.icons.user + ' ' + item.author + '</div>';
8522      }
8523      if( item.exif.model != '' ) {
8524        content  += '<div class="nGY2PopupOneItemText">' + G.O.icons.config + ' ' + item.exif.model + '</div>';
8525      }
8526      var sexif = '';
8527      sexif += item.exif.flash == '' ? '' : ' &nbsp; ' + item.exif.flash;
8528      sexif += item.exif.focallength == '' ? '' : ' &nbsp; ' + item.exif.focallength+'mm';
8529      sexif += item.exif.fstop == '' ? '' : ' &nbsp; f' + item.exif.fstop;
8530      sexif += item.exif.exposure == '' ? '' : ' &nbsp; ' + item.exif.exposure+'s';
8531      sexif += item.exif.iso == '' ? '' : ' &nbsp; ' + item.exif.iso+' ISO';
8532      if( item.exif.time != '' ) {
8533        // var date = new Date(parseInt(item.exif.time));
8534        // sexif += ' &nbsp; '+date.toLocaleDateString();
8535        sexif += ' &nbsp; ' + item.exif.time;
8536      }
8537      content += '<div class="nGY2PopupOneItemText">' + sexif + '</div>';
8538
8539      if( item.exif.location != '' ) {
8540        content += '<div class="nGY2PopupOneItemText">'+G.O.icons.location+' <a href="http://maps.google.com/maps?z=12&t=m&q='+encodeURIComponent(item.exif.location)+'" target="_blank">'+item.exif.location+'</a></div>';
8541        // embed google map in iframe (no api key required)
8542        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( item.exif.location ) +'&amp;output=embed"></iframe>';  
8543      }
8544
8545      Popup(G.O.icons.viewerInfo, content, 'Left');
8546   
8547    }
8548    
8549
8550    
8551    function ToolbarAddElt( elt ) {
8552      var r = '<div class="ngbt ngy2viewerToolAction ',
8553      e=elt.replace(/^\s+|\s+$/g, '');    // remove trailing/leading whitespace
8554      switch( e ) {
8555        case 'minimizeButton':
8556          var ic = G.O.icons.viewerToolbarMin;
8557          if( G.VOM.toolbarMode == 'min' ) {
8558            ic = G.O.icons.viewerToolbarStd;
8559          }
8560          r += 'minimizeButton nGEvent" data-ngy2action="minimize">'+ic+'</div>';
8561          break;
8562        case 'previousButton':
8563          r += 'previousButton nGEvent" data-ngy2action="previous">'+G.O.icons.viewerPrevious+'</div>';
8564          break;
8565        case 'pageCounter':
8566          r += 'pageCounter nGEvent"></div>';
8567          break;
8568        case 'nextButton':
8569          r += 'nextButton nGEvent" data-ngy2action="next">'+G.O.icons.viewerNext+'</div>';
8570          break;
8571        case 'playPauseButton':
8572          r += 'playButton playPauseButton nGEvent" data-ngy2action="playPause">'+G.O.icons.viewerPlay+'</div>';
8573          break;
8574        case 'rotateLeft':
8575          r += 'rotateLeftButton nGEvent" data-ngy2action="rotateLeft">'+G.O.icons.viewerRotateLeft+'</div>';
8576          break;
8577        case 'rotateRight':
8578          r += 'rotateRightButton nGEvent" data-ngy2action="rotateRight">'+G.O.icons.viewerRotateRight+'</div>';
8579          break;
8580        case 'downloadButton':
8581          r += 'downloadButton nGEvent" data-ngy2action="download">'+G.O.icons.viewerDownload+'</div>';
8582          break;
8583        case 'zoomButton':
8584          r += 'nGEvent" data-ngy2action="zoomIn">'+G.O.icons.viewerZoomIn+'</div><div class="ngbt ngy2viewerToolAction nGEvent" data-ngy2action="zoomOut">'+G.O.icons.viewerZoomOut+'</div>';
8585          break;
8586        case 'fullscreenButton':
8587          var s = G.O.icons.viewerFullscreenOn;
8588          if( ngscreenfull.enabled && G.VOM.viewerIsFullscreen ) {
8589            s = G.O.icons.viewerFullscreenOff;
8590          }
8591          r += 'setFullscreenButton fullscreenButton nGEvent" data-ngy2action="fullScreen">'+s+'</div>';
8592          break;
8593        case 'infoButton':
8594          r += 'infoButton nGEvent" data-ngy2action="info">'+G.O.icons.viewerInfo+'</div>';
8595          break;
8596        case 'linkOriginalButton':
8597          r += 'linkOriginalButton nGEvent" data-ngy2action="linkOriginal">' + G.O.icons.viewerLinkOriginal + '</div>';
8598          break;
8599        case 'closeButton':
8600          r += 'closeButton nGEvent" data-ngy2action="close">'+G.O.icons.buttonClose+'</div>';
8601          break;
8602        case 'shareButton':
8603          r += 'nGEvent" data-ngy2action="share">'+G.O.icons.viewerShare+'</div>';
8604          break;
8605        case 'label':
8606          r += '"><div class="title nGEvent" itemprop="name"></div><div class="description nGEvent" itemprop="description"></div></div>';
8607          break;
8608        default:
8609          // custom button
8610          if( e.indexOf('custom') == 0 ) {
8611            var t = '';
8612            // content to display from custom script
8613            var fu = G.O.fnImgToolbarCustInit;
8614            if( fu !== null ) {
8615              typeof fu == 'function' ? fu(e) : window[fu](e);
8616            }
8617            if( t == undefined || t == '' ) {
8618              // content from icons
8619              var n = e.substring(6);
8620              t = G.O.icons['viewerCustomTool'+n];
8621            }
8622            r += 'ngy2CustomBtn ' + e + ' nGEvent" data-ngy2action="' + e + '">' + t + '</div>';
8623          }
8624          else {
8625            r = '';
8626          }
8627          break;
8628      }
8629      return r;
8630    }
8631    
8632    
8633    // toggle slideshow mode on/off
8634    function SlideshowToggle(){
8635      if( G.VOM.playSlideshow ) {
8636        window.clearTimeout(G.VOM.playSlideshowTimerID);
8637        G.VOM.playSlideshow = false;
8638        G.VOM.$viewer.find('.playPauseButton').html(G.O.icons.viewerPlay);
8639      }
8640      else {
8641        G.VOM.playSlideshow = true;
8642        DisplayNextMedia();
8643        G.VOM.$viewer.find('.playPauseButton').html(G.O.icons.viewerPause);
8644      }
8645    }
8646
8647    function ViewerToolbarForVisibilityStd() {
8648      G.VOM.toolbarMode = 'std';
8649      
8650      var sTB = '';
8651      var t = G.O.viewerToolbar.standard.split(',');
8652      for( var i = 0, lt = t.length; i < lt; i++) {
8653        sTB += ToolbarAddElt( t[i] );
8654      }
8655      G.VOM.$toolbar.find('.toolbar').html(sTB);
8656      ViewerToolbarElementContent();
8657    }
8658    
8659    function ViewerToolbarForVisibilityMin() {
8660      if( G.O.viewerToolbar.minimized == undefined || G.O.viewerToolbar.minimized == '' ) {
8661        ViewerToolbarForVisibilityStd();
8662      }
8663      else {
8664        G.VOM.toolbarMode = 'min';
8665        var sTB = '';
8666        var t = G.O.viewerToolbar.minimized.split(',');
8667        for( var i = 0, lt = t.length; i < lt; i++) {
8668          sTB += ToolbarAddElt( t[i] );
8669        }
8670        G.VOM.$toolbar.find('.toolbar').html(sTB);
8671        ViewerToolbarElementContent();
8672      }
8673    }
8674    
8675    function ViewerToolbarElementContent() {
8676    
8677      var vomIdx=G.VOM.currItemIdx;
8678      if( vomIdx == null ) { return; }
8679      
8680      var item=G.VOM.NGY2Item(0);
8681    
8682      // LABEL
8683      var setTxt = false;
8684      // set title
8685      if( item.title !== undefined && item.title != '' ) {
8686        G.VOM.$viewer.find('.ngy2viewerToolAction').find('.title').html(item.title);
8687        setTxt = true;
8688      }
8689      else {
8690        G.VOM.$viewer.find('.ngy2viewerToolAction').find('.title').html('');
8691      }
8692      // set description
8693      if( item.description !== undefined && item.description != '' ) {
8694        // G.VOM.$viewer.find('.ngy2viewerToolAction').find('.description').html(item.description);
8695        // setTxt = true;
8696      }
8697      else {
8698        // G.VOM.$viewer.find('.ngy2viewerToolAction').find('.description').html('');
8699      }
8700      
8701      if( setTxt ) {
8702        G.VOM.$viewer.find('.ngy2viewerToolAction').find('.label').show();
8703      }
8704      else {
8705        G.VOM.$viewer.find('.ngy2viewerToolAction').find('.label').hide();
8706      }
8707      
8708      // set page number
8709      var viewerMaxImages = G.VOM.items.length;
8710      if( viewerMaxImages > 0 ) {
8711        G.VOM.$viewer.find('.pageCounter').html((G.VOM.items[vomIdx].mediaNumber)+'/'+viewerMaxImages);
8712      }
8713      
8714      // custom elements
8715      var $cu = G.VOM.$viewer.find('.ngy2CustomBtn');
8716      var fu = G.O.fnImgToolbarCustDisplay;
8717      if( $cu.length > 0 && fu !== null ) {
8718        typeof fu == 'function' ? fu($cu, item) : window[fu]($cu, item);
8719      }
8720      
8721      // set event handlers again
8722      ViewerToolsOn();
8723    }
8724    
8725    // Pan the media container in the lightbox (left/right)
8726    function ViewerMediaPanX( posX ) {
8727      G.VOM.swipePosX = posX;
8728      if( G.CSStransformName == null ) {
8729        // no pan if CSS transform not supported
8730        // G.VOM.$mediaCurrent.css({ left: posX }); 
8731      }
8732      else {
8733      
8734        // pan left/right the current media
8735        // window.ng_draf( function() {
8736          G.VOM.$mediaCurrent[0].style[G.CSStransformName] = 'translate(' + posX + 'px, 0px)';
8737        // });
8738
8739        var itemPrevious = G.VOM.NGY2Item(-1);
8740        var itemNext = G.VOM.NGY2Item(1);
8741        
8742        // next/previous media
8743        if(  G.O.imageTransition.startsWith('SWIPE') ) {
8744          if( itemPrevious.mediaTransition() ) {
8745            ViewerSetMediaVisibility(G.VOM.NGY2Item(-1), G.VOM.$mediaPrevious, 1);
8746          }
8747          if( itemNext.mediaTransition() ) {
8748            ViewerSetMediaVisibility(G.VOM.NGY2Item(1), G.VOM.$mediaNext, 1);
8749          }
8750
8751          var sc = Math.min( Math.max( Math.abs(posX) / G.VOM.window.lastWidth, .8), 1);
8752          if( G.O.imageTransition == 'SWIPE' ) { sc = 1; }
8753
8754          if( posX > 0 ) {
8755            var dir = G.VOM.window.lastWidth;
8756            if( itemPrevious.mediaTransition() ) {
8757              // window.ng_draf( function() {
8758                G.VOM.$mediaPrevious[0].style[G.CSStransformName] = 'translate(' + (-dir + posX) + 'px, 0px) scale(' + sc + ')';
8759              // });
8760            }
8761            if( itemNext.mediaTransition() ) {
8762              // window.ng_draf( function() {
8763                G.VOM.$mediaNext[0].style[G.CSStransformName] = 'translate(' + (dir) + 'px, 0px) scale(' + sc + ')';
8764              // });
8765            }
8766          }
8767          else {
8768            var dir = -G.VOM.window.lastWidth;
8769            if( itemNext.mediaTransition() ) {
8770              // window.ng_draf( function() {
8771                G.VOM.$mediaNext[0].style[G.CSStransformName] = 'translate(' + (-dir + posX) + 'px, 0px) scale(' + sc + ')';
8772              // });
8773            }
8774            if( itemPrevious.mediaTransition() ) {
8775              // window.ng_draf( function() {
8776                G.VOM.$mediaPrevious[0].style[G.CSStransformName] = 'translate(' + (dir) + 'px, 0px) scale(' + sc + ')';
8777              // });
8778            }
8779          }
8780        }
8781        
8782        
8783        if(  G.O.imageTransition == 'SLIDEAPPEAR' ) {
8784          G.VOM.$mediaPrevious[0].style[G.CSStransformName] = '';
8785          G.VOM.$mediaNext[0].style[G.CSStransformName] = '';
8786          if( posX < 0 ) {
8787            var o = (-posX) / G.VOM.window.lastWidth;
8788            if( itemNext.mediaTransition() ) {
8789              ViewerSetMediaVisibility(itemNext, G.VOM.$mediaNext, o);
8790            }
8791            if( itemPrevious.mediaTransition() ) {
8792              ViewerSetMediaVisibility(itemPrevious, G.VOM.$mediaPrevious, 0);
8793            }
8794          }
8795          else {
8796            var o = posX / G.VOM.window.lastWidth;
8797            if( itemPrevious.mediaTransition() ) {
8798              ViewerSetMediaVisibility(itemPrevious, G.VOM.$mediaPrevious, o);
8799            }
8800            if( itemNext.mediaTransition() ) {
8801              ViewerSetMediaVisibility(itemNext, G.VOM.$mediaNext, 0);
8802            }
8803          }
8804        }
8805      }
8806    }
8807    
8808    // Display next image
8809    function DisplayNextMedia() {
8810      if( G.VOM.viewerMediaIsChanged || ((new Date().getTime()) - G.VOM.timeImgChanged < 300) ) { return; }
8811      
8812      TriggerCustomEvent('lightboxNextImage');
8813      DisplayInternalViewer(G.VOM.IdxNext(), 'nextImage');
8814    };
8815    
8816    // Display previous image
8817    function DisplayPreviousMedia() {
8818      if( G.VOM.viewerMediaIsChanged || ((new Date().getTime()) - G.VOM.timeImgChanged < 300) ) { return; }
8819      if( G.VOM.playSlideshow ) {
8820        SlideshowToggle();
8821      }
8822      
8823      TriggerCustomEvent('lightboxPreviousImage');
8824      DisplayInternalViewer(G.VOM.IdxPrevious(), 'previousImage');
8825    };
8826    
8827    // Display image (and run animation)
8828    function DisplayInternalViewer( newVomIdx, displayType ) {
8829
8830      if( G.VOM.playSlideshow ) { window.clearTimeout(G.VOM.playSlideshowTimerID); }
8831      
8832      var itemOld = G.VOM.NGY2Item(0);
8833      var itemNew = G.I[G.VOM.items[newVomIdx].ngy2ItemIdx];
8834      var $new = (displayType == 'nextImage' ? G.VOM.$mediaNext : G.VOM.$mediaPrevious);
8835      if( displayType == 'nextImage' ) {
8836        ViewerSetMediaVisibility(G.VOM.NGY2Item(-1), G.VOM.$mediaPrevious, 0);
8837      }
8838      else {
8839        ViewerSetMediaVisibility(G.VOM.NGY2Item(1), G.VOM.$mediaNext, 0);
8840      }
8841
8842      G.VOM.timeImgChanged = new Date().getTime();
8843      G.VOM.viewerMediaIsChanged = true;
8844      G.VOM.zoom.isZooming = false;
8845      ResizeInternalViewer(true);
8846 
8847      if( G.O.debugMode && console.timeline ) { console.timeline('nanogallery2_viewer'); }
8848
8849      SetLocationHash( itemNew.albumID, itemNew.GetID() );
8850      
8851      // animation duration is proportional of the remaining distance
8852      var vP = G.GOM.cache.viewport;
8853      var dur = 400 * (vP.w - Math.abs(G.VOM.swipePosX)) / vP.w;
8854      
8855      if( displayType == '' ) {
8856        // first image --> just appear / no slide animation
8857        ViewerSetMediaVisibility(G.VOM.NGY2Item(0), G.VOM.$mediaCurrent, 1);
8858        if( G.CSStransformName == null ) {
8859          // no CSS transform support -> no animation
8860          ViewerSetMediaVisibility(itemNew, $new, 1);
8861          DisplayInternalViewerComplete(displayType, newVomIdx);
8862        }
8863        else {
8864          ViewerSetMediaVisibility(itemNew, $new, 0);
8865          new NGTweenable().tween({
8866            from:         { opacity: 0 },
8867            to:           { opacity: 1 },
8868            attachment:   { dT: displayType, item: itemOld },
8869            easing:       'easeInOutSine',
8870            delay:        30,
8871            duration:     400,
8872            step:         function (state, att) {
8873              // using scale is not a good idea on Chrome -> image will be blurred
8874              G.VOM.$content.css('opacity', state.opacity);
8875            },
8876            finish:       function (state, att) {
8877              ViewerToolsUnHide();
8878              DisplayInternalViewerComplete(att.dT, newVomIdx);
8879            }
8880          });
8881        }
8882      }
8883      else {
8884        // animate the image transition between 2 images
8885        if( G.CSStransformName == null  ) {
8886          // no CSS transform support -> no animation
8887          ViewerSetMediaVisibility(itemNew, $new, 1);
8888          ViewerSetMediaVisibility(G.VOM.NGY2Item(0), G.VOM.$mediaCurrent, 1);
8889          DisplayInternalViewerComplete(displayType, newVomIdx);
8890        }
8891        else {
8892          switch( G.O.imageTransition ) {
8893            case 'SWIPE':
8894            case 'SWIPE2':
8895              var dir = ( displayType == 'nextImage' ? - vP.w : vP.w );
8896              $new[0].style[G.CSStransformName] = 'translate('+(-dir)+'px, 0px) '
8897
8898              new NGTweenable().tween({
8899                from:         { t: G.VOM.swipePosX  },
8900                to:           { t: (displayType == 'nextImage' ? - vP.w : vP.w) },
8901                attachment:   { dT: displayType, $e: $new, itemNew: itemNew, itemOld: itemOld, dir: dir },
8902                delay:        30,
8903                duration:     dur,
8904                easing:       (G.O.imageTransition == 'swipe' ? 'easeInOutSine' : 'easeInOutCubic'),
8905                step:         function (state, att) {
8906                  // current media
8907                  ViewerSetMediaVisibility(G.VOM.NGY2Item(0), G.VOM.$mediaCurrent, 1);
8908                  // G.VOM.$mediaCurrent[0].style[G.CSStransformName] = 'translate(' + state.t + 'px, 0px) rotate(' + att.itemOld.rotationAngle + 'deg)';
8909                  G.VOM.$mediaCurrent[0].style[G.CSStransformName] = 'translate(' + state.t + 'px, 0px)';
8910                  // new media
8911                  if( att.itemNew.mediaTransition() ) {
8912                    ViewerSetMediaVisibility(att.itemNew, att.$e, 1);
8913
8914                    var sc = Math.min( Math.max( (Math.abs(state.t)) / G.VOM.window.lastWidth, .8), 1);
8915                    if( G.O.imageTransition == 'SWIPE' ) { sc = 1; }
8916                    // att.$e[0].style[G.CSStransformName] = 'translate(' + (-att.dir+state.t) + 'px, 0px) scale(' + sc + ') rotate(' + att.itemNew.rotationAngle + 'deg)';
8917                    att.$e[0].style[G.CSStransformName] = 'translate(' + (-att.dir+state.t) + 'px, 0px) scale(' + sc + ')';
8918                  }
8919                },
8920                finish:       function (state, att) {
8921                  G.VOM.$mediaCurrent[0].style[G.CSStransformName] = '';
8922                  att.$e[0].style[G.CSStransformName] = '';
8923                  DisplayInternalViewerComplete(att.dT, newVomId
8923x);
8924                }
8925              });
8926              break;
8927              
8928            case 'SLIDEAPPEAR':
8929            default:
8930              var dir=(displayType == 'nextImage' ? - vP.w : vP.w);
8931              var op = (Math.abs(G.VOM.swipePosX)) / G.VOM.window.lastWidth;
8932              $new[0].style[G.CSStransformName] = '';
8933              new NGTweenable().tween({
8934                from:         { o: op, t: G.VOM.swipePosX },
8935                to:           { o: 1, t: (displayType == 'nextImage' ? - vP.w : vP.w) },
8936                attachment:   { dT:displayType, $e:$new, itemNew: itemNew, itemOld: itemOld, dir: dir },
8937                delay:        30,
8938                duration:     dur,
8939                easing:       'easeInOutSine',
8940                step:         function (state, att) {
8941                  // current media - translate
8942                  // G.VOM.$mediaCurrent[0].style[G.CSStransformName]= 'translate('+state.t+'px, 0px) rotate(' + att.itemOld.rotationAngle + 'deg)';
8943                  G.VOM.$mediaCurrent[0].style[G.CSStransformName]= 'translate('+state.t+'px, 0px)';
8944                  // new media - opacity
8945                  if( att.itemNew.mediaTransition() ) {
8946                    ViewerSetMediaVisibility(att.itemNew, att.$e, state.o);
8947                  }
8948                },
8949                finish:       function (state, att) {
8950                  G.VOM.$mediaCurrent[0].style[G.CSStransformName]= '';
8951                  DisplayInternalViewerComplete(att.dT, newVomIdx);
8952                }
8953              });
8954              break;
8955          }
8956        }
8957      }
8958    }
8959  
8960
8961    function DisplayInternalViewerComplete( displayType, newVomIdx ) {
8962      G.VOM.currItemIdx = newVomIdx;
8963      
8964      var ngy2item = G.VOM.NGY2Item(0);
8965
8966      ViewerToolbarElementContent();
8967      if( G.O.debugMode && console.timeline ) { console.timelineEnd('nanogallery2_viewer'); }
8968
8969      var fu=G.O.fnImgDisplayed;
8970      if( fu !== null ) {
8971        typeof fu == 'function' ? fu(ngy2item) : window[fu](ngy2item);
8972      }
8973      
8974      G.VOM.swipePosX = 0;
8975      if( displayType != '' ) {
8976        // not on first media display
8977        // G.VOM.$mediaCurrent.off("click");
8978        G.VOM.$mediaCurrent.removeClass('imgCurrent');
8979
8980        var $tmp = G.VOM.$mediaCurrent;
8981        switch( displayType ) {
8982          case 'nextImage':
8983            G.VOM.$mediaCurrent = G.VOM.$mediaNext;
8984            G.VOM.$mediaNext = $tmp;
8985            break;
8986          case 'previousImage':
8987            G.VOM.$mediaCurrent = G.VOM.$mediaPrevious;
8988            G.VOM.$mediaPrevious = $tmp;
8989            break;
8990        }
8991      }
8992      
8993      G.VOM.$mediaCurrent.addClass('imgCurrent');
8994      
8995      // re-sort the media containers --> current on top
8996      var $pans = G.VOM.$content.find('.nGY2ViewerMediaPan');
8997      G.VOM.$mediaCurrent.insertAfter($pans.last());
8998      
8999      if( ngy2item.mediaKind == 'img' && ngy2item.imageWidth == 0 ) {
9000        ViewerSetMediaVisibility(G.VOM.NGY2Item(0), G.VOM.$mediaCurrent, 0);
9001      }
9002      else {
9003        G.VOM.$mediaCurrent.children().eq(0).attr('class', 'nGY2ViewerMediaLoaderHidden');    // hide preloader
9004        ViewerSetMediaVisibility(G.VOM.NGY2Item(0), G.VOM.$mediaCurrent, 1);
9005      }
9006
9007      
9008      // set the new NEXT media
9009      G.VOM.$mediaNext.empty();
9010      var nextItem = G.VOM.NGY2Item(1);
9011      var spreloader = '<div class="nGY2ViewerMediaLoaderDisplayed"></div>';
9012      if( nextItem.mediaKind == 'img' && nextItem.imageWidth != 0 && nextItem.imageHeight != 0 ) {
9013        spreloader = '<div class="nGY2ViewerMediaLoaderHidden"></div>';
9014      }
9015      G.VOM.$mediaNext.append( spreloader + nextItem.mediaMarkup );
9016      ViewerSetMediaVisibility(nextItem, G.VOM.$mediaNext, 0);
9017      if( nextItem.mediaKind == 'img' ) {
9018        G.VOM.ImageLoader.loadImage(VieweImgSizeRetrieved, nextItem);
9019      }
9020      else {
9021        ViewerMediaCenterNotImg( G.VOM.$mediaNext );
9022      }
9023
9024      // set the new PREVIOUS media
9025      G.VOM.$mediaPrevious.empty();
9026      var previousItem = G.VOM.NGY2Item(-1);
9027      spreloader = '<div class="nGY2ViewerMediaLoaderDisplayed"></div>';
9028      if( previousItem.mediaKind == 'img' && previousItem.imageWidth != 0 && previousItem.imageHeight != 0 ) {
9029        spreloader = '<div class="nGY2ViewerMediaLoaderHidden"></div>';
9030      }
9031      G.VOM.$mediaPrevious.append( spreloader + previousItem.mediaMarkup );
9032      ViewerSetMediaVisibility(previousItem, G.VOM.$mediaPrevious, 0);
9033      if( previousItem.mediaKind == 'img' ) {
9034        G.VOM.ImageLoader.loadImage( VieweImgSizeRetrieved, previousItem );
9035      }
9036      else {
vendor: 16,252 bytes, lines 9037-9370
9037        ViewerMediaCenterNotImg( G.VOM.$mediaPrevious );
9038      }
9039
9040        
9041      // slideshow mode - wait until image is loaded to start the delay for next image
9042      if( G.VOM.playSlideshow ) {
9043        G.VOM.$mediaCurrent.children().eq(1).ngimagesLoaded().always( function( instance ) {
9044          if( G.VOM.playSlideshow ) {
9045            // in the meantime the user could have stopped the slideshow
9046            G.VOM.playSlideshowTimerID = window.setTimeout( function(){ DisplayNextMedia(); }, G.VOM.slideshowDelay );
9047          }
9048        });
9049      }
9050      
9051      // close viewer when user clicks outside of the image
9052      // G.VOM.$mediaCurrent.on("click",function(e){
9053      //  e.stopPropagation();
9054      //  if( (new Date().getTime()) - G.VOM.timeImgChanged < 400 ) { return; }
9055      //  StopPropagationPreventDefault(e);
9056      //  CloseInternalViewer(G.VOM.currItemIdx);
9057      //  return false;
9058      // });
9059
9060      ResizeInternalViewer();
9061
9062      G.VOM.viewerMediaIsChanged = false;
9063      TriggerCustomEvent('lightboxImageDisplayed');
9064      
9065    }
9066
9067    
9068    // Is fired as soon as the size of an image has been retrieved (the download may not be finished)
9069    function VieweImgSizeRetrieved(w, h, item, n) {
9070      item.imageWidth = w;
9071      item.imageHeight = h;
9072  
9073      // image sized retrieved for currently displayed media
9074      // if( G.VOM.$mediaCurrent !== null && G.VOM.$mediaCurrent.children().attr('src') == item.responsiveURL() ) {
9075      if( G.VOM.NGY2Item(0) == item ) {
9076        // it is the current displayed media
9077        G.VOM.$mediaCurrent.children().eq(0).attr('class', 'nGY2ViewerMediaLoaderHidden');    // hide preloader
9078        ViewerSetMediaVisibility(G.VOM.NGY2Item(0), G.VOM.$mediaCurrent, 1);
9079        G.VOM.zoom.userFactor = 1;
9080      }
9081      
9082      if( G.VOM.NGY2Item(1) == item ) {   // next media
9083        G.VOM.$mediaNext.children().eq(0).attr('class', 'nGY2ViewerMediaLoaderHidden');    // hide preloader
9084      }
9085      if( G.VOM.NGY2Item(-1) == item ) {   // previous media
9086        G.VOM.$mediaPrevious.children().eq(0).attr('class', 'nGY2ViewerMediaLoaderHidden');    // hide preloader
9087      }
9088      
9089      ViewerMediaSetPosAndZoom();
9090
9091    }
9092
9093    // Viewer - Set the visibility of the media and it's container
9094    function ViewerSetMediaVisibility(item, $media, opacity ) {
9095      
9096      if( item.mediaKind == 'img' && item.imageWidth == 0 ) {
9097        // do not display image if width is unknown (--> callback will set the width when know)
9098        // setting opacity to 0 is not enough -> it is mandatory to change also the visibility to hidden to avoid responds to events (click/touch)
9099        // $media.children().css({ opacity: 0, visibility: 'hidden' });
9100        $media.children().eq(1).css({ opacity: 0, visibility: 'hidden' });    // hide media
9101        // $media.css({ opacity: 0, visibility: 'hidden' });
9102        return;
9103      }
9104      
9105      if( opacity == 0 ) {
9106        // setting opacity to 0 is not enough -> it is mandatory to change also the visibility to hidden to avoid responds to events (click/touch)
9107        // $media.css({ opacity: 0, visibility: 'hidden' });
9108        $media.children().css({ opacity: 0, visibility: 'hidden' });    // hide media
9109      }
9110      else {
9111        // $media.css({ opacity: opacity, visibility: 'visible' });
9112        $media.children().css({ opacity: opacity, visibility: 'visible' });      // display media
9113      }
9114    }
9115    
9116    
9117    // Close the internal lightbox
9118    function CloseInternalViewer( vomIdx ) {
9119
9120      G.VOM.viewerMediaIsChanged = false;
9121
9122      if( G.VOM.viewerDisplayed ) {
9123
9124        // set scrollbar visible again
9125      jQuery('body').css({ overflowX: G.VOM.saveOverflowX, overflowY: G.VOM.saveOverflowY});
9126      // jQuery('body').css({overflow: 'visible'});
9127        
9128        
9129        if( G.VOM.playSlideshow ) {
9130          window.clearTimeout(G.VOM.playSlideshowTimerID);
9131          G.VOM.playSlideshow = false;
9132        }
9133
9134        // G.VOM.userEvents.removeEventListeners();
9135        // G.VOM.userEvents=null;
9136        G.VOM.hammertime.destroy();
9137        G.VOM.hammertime = null;
9138
9139        if( ngscreenfull.enabled && G.VOM.viewerIsFullscreen ) {
9140          G.VOM.viewerIsFullscreen = false;
9141          ngscreenfull.exit();
9142        }
9143        
9144        G.VOM.$cont.hide(0).off().show(0).html('').remove();
9145        G.VOM.viewerDisplayed = false;
9146
9147        if( G.O.thumbnailAlbumDisplayImage ) {
9148          // content of album displayed directly in lightbox (no gallery display for album content)
9149          if( vomIdx == null ) {
9150            // lightbox closed with browser back-button
9151            // the gallery is already displayed
9152          }
9153          else {
9154            var item = G.I[G.VOM.items[vomIdx].ngy2ItemIdx];
9155            var parent = NGY2Item.Get(G, item.albumID);
9156            if( G.GOM.albumIdx != parent.albumID ) {
9157              // display album only if not already displayed
9158              DisplayAlbum('-1', parent.albumID);
9159            }
9160            else {
9161              GalleryResize();        
9162              SetLocationHash( '', '' );
9163              ThumbnailHoverReInitAll();
9164            }
9165          }
9166            // DisplayAlbum( '-', G.I[G.VOM.items[vomIdx].ngy2ItemIdx].albumID );
9167        }
9168        else {
9169          if( vomIdx != null ) {
9170            if( G.GOM.albumIdx == -1 ) {
9171              // album not displayed --> display gallery
9172              DisplayAlbum( '', G.I[G.VOM.items[vomIdx].ngy2ItemIdx].albumID );
9173            }
9174            else {
9175              GalleryResize();        
9176              SetLocationHash( G.I[G.VOM.items[vomIdx].ngy2ItemIdx].albumID, '' );
9177              ThumbnailHoverReInitAll();
9178            }
9179          }
9180        }
9181        G.VOM.timeImgChanged = new Date().getTime();
9182      }
9183    }
9184
9185    
9186    // Internal viewer resized -> reposition elements
9187    function ResizeInternalViewer( forceUpdate ) {
9188      forceUpdate = typeof forceUpdate !== 'undefined' ? forceUpdate : false;
9189      
9190      if( G.VOM.$toolbar === null ) { return; }   // viewer build not finished
9191      
9192      
9193      // window.requestAnimationFrame( function() {    // synchronize with screen
9194      var windowsW = G.VOM.$viewer.width();
9195      var windowsH = G.VOM.$viewer.height();
9196      var $elt = G.VOM.$mediaCurrent.children().eq(1);
9197      if( $elt == null || G.VOM.currItemIdx == -1 ) { return; }
9198      
9199      if( !forceUpdate && G.VOM.window.lastWidth == windowsW  && G.VOM.window.lastHeight == windowsH ) { return; }
9200      
9201      G.VOM.window.lastWidth = windowsW;
9202      G.VOM.window.lastHeight = windowsH;
9203
9204      // var vwImgC_H=$elt.height(),
9205      // vwImgC_W=$elt.width(),
9206      // vwImgC_OHt=$elt.outerHeight(true),
9207      // vwImgC_OHf=$elt.outerHeight(false);
9208
9209      var $tb = G.VOM.$toolbar.find('.toolbar');
9210      var tb_OHt = $tb.outerHeight(true);
9211
9212      switch( G.O.viewerToolbar.position ) {
9213        case 'topOverImage':
9214          G.VOM.$content.css({height: windowsH, width: windowsW, top: 0  });
9215          G.VOM.$toolbar.css({top: 0, bottom: ''});
9216          break;
9217        case 'top':
9218          windowsH -= tb_OHt;
9219          G.VOM.$content.css({height: windowsH, width: windowsW, top: tb_OHt  });
9220          G.VOM.$toolbar.css({top: 0});
9221          break;
9222        case 'bottomOverImage':
9223          G.VOM.$content.css({height:windowsH, width: windowsW, bottom: 0, top: 0  });
9224          G.VOM.$toolbar.css({bottom: 0});
9225          break;
9226        case 'bottom':
9227        default:
9228          windowsH -= tb_OHt;
9229          G.VOM.$content.css({ width: windowsW, top: 0, bottom: tb_OHt });
9230          G.VOM.$toolbar.css({bottom: 0});
9231          break;
9232      }
9233
9234      if( !G.VOM.viewerMediaIsChanged && G.VOM.zoom.isZooming ) {
9235        ViewerMediaSetPosAndZoom();
9236      }
9237      else {
9238        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 )) {
9239          // animate image zoom factor and position back to initial values
9240          G.VOM.zoom.isZooming= true;   // activate zooming temporarily
9241          new NGTweenable().tween({
9242            from:           { userFactor: G.VOM.zoom.userFactor, panPosX: G.VOM.panPosX, panPosY: G.VOM.panPosY, zoomPosX: G.VOM.zoom.posX, zoomPosY: G.VOM.zoom.posY },
9243            to:           { userFactor: 1, panPosX: 0, panPosY: 0, zoomPosX: 0, zoomPosY: 0 },
9244            easing:       'easeInOutSine',
9245            delay:        0,
9246            duration:     150,
9247            step:         function (state) {
9248              G.VOM.zoom.userFactor=state.userFactor;
9249              G.VOM.panPosX=state.panPosX;
9250              G.VOM.panPosY=state.panPosY;
9251              G.VOM.zoom.posX=state.zoomPosX;
9252              G.VOM.zoom.posY=state.zoomPosY;
9253              ViewerMediaSetPosAndZoom();
9254            },
9255            finish:       function (state) {
9256              G.VOM.zoom.isZooming=false;
9257            }
9258          });
9259      
9260        }
9261        else {
9262          G.VOM.zoom.userFactor = 1;
9263          G.VOM.zoom.isZooming = false;
9264          G.VOM.panPosX = 0;
9265          G.VOM.panPosY = 0;
9266          G.VOM.zoom.posX = 0;
9267          G.VOM.zoom.posY = 0;
9268          ViewerMediaSetPosAndZoom();
9269        }
9270      }
9271    }
9272
9273
9274
9275    /** @function BuildSkeleton */
9276    /** Build the gallery structure **/
9277    function BuildSkeleton() {
9278    
9279      // store markup if defined
9280      var $elements = G.$E.base.children('a');
9281      if( $elements.length > 0 ) {
9282        G.O.$markup = $elements;
9283      }
9284      G.$E.base.text('');
9285      G.$E.base.addClass('ngy2_container');
9286      
9287      // RTL or LTR
9288      var sRTL='';
9289      if( G.O.RTL ) {
9290        sRTL = 'style="text-align:right;direction:rtl;"';
9291      }
9292    
9293      // theme
9294      G.$E.base.addClass(G.O.theme)
9295      // gallery color scheme
9296      SetGalleryTheme();
9297
9298      // Hide icons (thumbnails and breadcrumb)
9299      if( G.O.thumbnailLabel.get('hideIcons') ) {
9300        G.O.icons.thumbnailAlbum = '';
9301        G.O.icons.thumbnailImage = '';
9302      }
9303
9304      // Navigation bar
9305      var styleNavigation="";
9306      if( G.O.navigationFontSize != undefined && G.O.navigationFontSize != '' ) {
9307        styleNavigation=' style="font-size:'+G.O.navigationFontSize+';"';
9308      }      
9309      G.$E.conNavigationBar = jQuery('<div class="nGY2Navigationbar" '+styleNavigation+'></div>').appendTo(G.$E.base);
9310
9311      // pre-loader
9312      G.$E.conLoadingB = jQuery('<div class="nanoGalleryLBarOff"><div></div><div></div><div></div><div></div><div></div></div>').appendTo(G.$E.base);
9313
9314      // gallery
9315      G.$E.conTnParent = jQuery('<div class="nGY2Gallery"></div>').appendTo( G.$E.base );
9316      G.$E.conTn = jQuery('<div class="nGY2GallerySub"></div>').appendTo( G.$E.conTnParent );
9317
9318      // configure gallery
9319      switch( G.O.thumbnailAlignment ) {
9320        case 'left':
9321          G.$E.conTnParent.css({'text-align':'left'});
9322          // G.$E.conNavBCon.css({'margin-left':0 });
9323          break;
9324        case 'right':
9325          G.$E.conTnParent.css({'text-align':'right'});
9326          // G.$E.conNavBCon.css({ 'margin-right':0});
9327          break;
9328      }
9329      
9330      // apply galleryBuildInit2 css settings to the gallery
9331      if( G.O.galleryBuildInit2 !== undefined ) {
9332      var t1=G.O.galleryBuildInit2.split('|');
9333        for( var i=0; i<t1.length; i++ ) {
9334          var o1=t1[i].split('_');
9335          if( o1.length == 2 ) {
9336            G.$E.conTn.css(o1[0], o1[1]);
9337          }
9338        }
9339      }
9340      
9341      // configure gallery depending on some thumbnail hover effects
9342      var effects=G.tn.hoverEffects.std.concat(G.tn.hoverEffects.level1);
9343      for( var j=0; j<effects.length; j++) {
9344        switch( effects[j].type ) {
9345          case 'scale':
9346          case 'rotateZ':
9347          case 'rotateX':
9348          case 'rotateY':
9349          case 'translateX':
9350          case 'translateY':
9351            // handle some special cases
9352            if( effects[j].element == '.nGY2GThumbnail' ) {
9353              // allow thumbnail upscale over the gallery's aera
9354              G.$E.base.css('overflow', 'visible');
9355              G.$E.base.find('.nGY2GallerySub').css('overflow', 'visible');
9356              G.$E.conTnParent.css('overflow', 'visible');
9357            }
9358            break;
9359        }
9360      }
9361      
9362      // Gallery bottom container
9363      G.$E.conTnBottom = jQuery('<div class="nGY2GalleryBottom" '+styleNavigation+'></div>').appendTo( G.$E.conTnParent );
9364      
9365      // portable edition
9366      if( G.O.portable ) {
9367        // http://www.picresize.com/
9368        // http://base64encode.net/base64-image-encoder
9369        // 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';
9370        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/Ow1lJfX088Hmd2dpZcLvda
9370Bl4pgQChH4B1iHU4a8E6Qj9ARGhpaUFrzeDgIJFIBGMM1lqMMWQyGSYmJohEIqxfv/7314CIoADtGTAaZTTaLI2VUhw+fBjnHBcvXuTy5cs45/A8j3Q6zcmTJ/F9n71799LW1rbgSOs3D+B1lBljcM7R3d3N0aNHKRQKnDhxgs7OTnbt2sX27dsZGRkhHo/T19e3+Kt/fQ1YawFwzolSCs/zUEqVtX1VcJcuXSKRSNDf3086nS6v79mzh76+Pjo6OigWi1RXV2OMWZC29PL8/PxSAL7vE41Gf4rFYkpEePToEb7vU1VVxW+ht7eXs2fPcv/+fQqFAps2baKlpaW8Xl1dTS6XY3x8HBFxtbW1BiiW4hAlInp8fNxt2LChPZvN/ru9vT2Sz+e93bt3qx07diwrzJWYcM5RU1NDNBots5bP53HOlS+kO3fuMDIy4hKJhKTT6ena2tqtxWJxoqamRr98HX9x7do1qaurExYaiXCVzK5bt04GBwdFRP728nVcWZAO+Hsmk/nsxo0bTTMzM5FXHZ83hYhQX1/vHzx48H9tbW1ngSsVvpYCmJ2dJRaLKRbapjpgOxB7K+9LmAbuAnOAnpiYcI2NjUsRLlo2myUMQ1M5t5rmnDO3bt1aNlfmd4W2XL/0/H8pUDF2rNCW/wLRuCkxx8V6wgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNy0wOC0wM1QwNDozMjoyMi0wNDowMO7mdkwAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTctMDgtMDNUMDQ6MzI6MjItMDQ6MDCfu87wAAAAAElFTkSuQmCC';
9371        var st = "font-weight:bold !important;color: #FF0075 !important;font-size: 14px !important;text-transform: lowercase !important;cursor:pointer !important;text-align: center !important;Text-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;";
9372        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);
9373        
9374        G.$E.ngy2i.find('a').on({
9375          mouseenter: function () {
9376            jQuery(this).attr('style', st);
9377          },
9378          mouseleave: function () {
9379            jQuery(this).attr('style', st);
9380          }
9381        });
9382      }
9383      
9384      // Error console
9385      G.$E.conConsole = jQuery('<div class="nGY2ConsoleParent"></div>').appendTo(G.$E.base);
9386
9387      // i18n translations
9388      i18n();
9389
9390      // cache some thumbnails data (sizes, styles...)
9391      ThumbnailDefCaches();
9392
9393      // do special settings depending for some options
9394      // thumbnail display transition
9395      switch( G.tn.opt.Get('displayTransition') ) {
9396        case 'SCALEDOWN':
9397        case 'RANDOMSCALE':
9398        default:
9399          G.$E.base.css('overflow', 'visible');
9400          G.$E.conTnParent.css('overflow', 'visible');
9401          G.$E.conTn.css('overflow', 'visible');
9402          break;
9403      }
9404      
9405    }
9406    
9407    function TriggerCustomEvent ( eventName ) {
9408      // G.$E.base.trigger('pageChanged.nanogallery2', new Event('pageChanged.nanogallery2'));
9409      var eN = eventName + '.nanogallery2';
9410      var event=null;
9411      try {
9412          event = new Event( eN );
9413        } catch(e) {
9414          event = document.createEvent('Event');
9415          event.initEvent(eN, false, false);
9416        }
9417        G.$E.base.trigger(eN, event);
9418    }
9419    
9420    
9421    /** @function SetGlobalEvents */
9422    function SetGlobalEvents() {
9423      // GLOBAL EVENT MANAGEMENT
9424      
9425      G.$E.conTnParent.on({
9426        mouseenter: GalleryMouseEnter,
9427        mouseleave: GalleryMouseLeave
9428      }, ".nGY2GThumbnail");    //pass the element as an argument to .on
9429      
vendor: 9,844 bytes, lines 9430-9688
9430      // G.GOM.hammertime = new NGHammer(G.$E.conTn[0], { touchAction: 'none' });
9431      G.GOM.hammertime = new NGHammer( G.$E.conTn[0] );
9432      // G.GOM.hammertime.domEvents = true;
9433      
9434      G.GOM.hammertime.on('pan', function(ev) {
9435        if( !G.VOM.viewerDisplayed ) {
9436          if( G.O.paginationSwipe && G.layout.support.rows && G.galleryDisplayMode.Get() == 'PAGINATION' ) {
9437            if( Math.abs(ev.deltaY) > G.GOM.panThreshold ) {
9438              G.GOM.panYOnly = true;
9439            }
9440            if( !G.GOM.panYOnly ) {
9441              G.$E.conTn.css( G.CSStransformName , 'translate('+(ev.deltaX)+'px,0px)');
9442            }
9443          }
9444        }
9445      });
9446      G.GOM.hammertime.on('panend', function(ev) {
9447        if( !G.VOM.viewerDisplayed ) {
9448          if( G.O.paginationSwipe && G.layout.support.rows && G.galleryDisplayMode.Get() == 'PAGINATION' ) {
9449            if( !G.GOM.panYOnly ) {
9450              if( ev.deltaX > 50 ) {
9451                paginationPreviousPage();
9452                return;
9453              }
9454              if(  ev.deltaX < -50 ) {
9455                paginationNextPage();
9456                return;
9457              }
9458            }
9459            G.GOM.panYOnly = false;
9460            G.$E.conTn.css( G.CSStransformName , 'translate(0px,0px)');
9461            // pX=0;
9462          }
9463        }
9464      });
9465      G.GOM.hammertime.on('tap', function(ev) {
9466        if( !G.VOM.viewerDisplayed ) {
9467          ev.srcEvent.stopPropagation();
9468          ev.srcEvent.preventDefault();  // cancel  mouseenter event
9469
9470          if( ev.pointerType == 'mouse') {
9471            if( GalleryClicked(ev.srcEvent) == 'exit' ) { return; }
9472          }
9473          else {
9474            var r = GalleryEventRetrieveElementl(ev.srcEvent, false);
9475            if( r.GOMidx == -1 ) { return; }
9476            if( r.action != 'NONE' && r.action != 'OPEN' ) {
9477              // toolbar touched --> execute action
9478              GalleryClicked(ev.srcEvent);
9479              return;
9480            }
9481            
9482            if( G.GOM.slider.hostIdx == r.GOMidx ) {
9483              // touch on thumbnail slider -> open immediately
9484              ThumbnailHoverOutAll();
9485              ThumbnailOpen(G.GOM.items[G.GOM.slider.currentIdx].thumbnailIdx, true);
9486              return;
9487            }
9488
9489            if( (G.GOM.curNavLevel == 'l1' && G.O.touchAnimationL1 == false) ||  (G.GOM.curNavLevel == 'lN' && G.O.touchAnimation == false) ) {
9490              // open on single touch (no hover animation)
9491              ThumbnailOpen(G.GOM.items[r.GOMidx].thumbnailIdx, true);
9492              return;
9493            }
9494            
9495            if( G.O.touchAutoOpenDelay > 0 ) {
9496              // open on single touch after end of hover animation (=defined delay)
9497              ThumbnailHoverOutAll();
9498              ThumbnailHover( r.GOMidx );
9499              window.clearInterval( G.touchAutoOpenDelayTimerID );
9500              G.touchAutoOpenDelayTimerID = window.setInterval(function(){
9501                window.clearInterval( G.touchAutoOpenDelayTimerID );
9502                ThumbnailOpen( G.GOM.items[r.GOMidx].thumbnailIdx, true );
9503              }, G.O.touchAutoOpenDelay );
9504            }
9505            else {
9506              // two touch scenario
9507              if( !G.I[G.GOM.items[r.GOMidx].thumbnailIdx].hovered ) {
9508                ThumbnailHoverOutAll();
9509                ThumbnailHover(r.GOMidx);
9510              }
9511              else {
9512                // second touch
9513                ThumbnailOpen(G.GOM.items[r.GOMidx].thumbnailIdx, true);
9514              }
9515            }
9516          }
9517        }
9518      });
9519      
9520      
9521      // browser location hash management
9522      if( G.O.locationHash ) {
9523        // jQuery(window).bind( 'hashchange', function() {
9524          // ProcessLocationHash();
9525        // });
9526        jQuery(window).on('hashchange.nanogallery2.' + G.baseEltID, function() {ProcessLocationHash();} );
9527      }
9528      
9529      // Page resize / orientation change
9530      jQuery(window).on('resize.nanogallery2.' + G.baseEltID + ' orientationChange.nanogallery2.' + G.baseEltID, debounce( ResizeWindowEvent, G.O.eventsDebounceDelay, false) );
9531      
9532      // Event page scrolled
9533      jQuery(window).on('scroll.nanogallery2.' + G.baseEltID, debounce( OnScrollEvent, G.O.eventsDebounceDelay, false) );
9534
9535      // Debounced function to hide the toolbars on the viewer
9536      G.VOM.toolsHide = debounce( ViewerToolsHide, G.O.viewerHideToolsDelay, false );
9537      
9538      // Keyboard management
9539      jQuery(document).keyup(function(e) {
9540        if( G.popup.isDisplayed ) {
9541          switch( e.keyCode) {
9542            case 27:    // Esc key
9543              G.popup.close();
9544              break;
9545          }
9546        }
9547        else {
9548          if( G.VOM.viewerDisplayed ) {
9549            ViewerToolsUnHide();
9550            switch( e.keyCode) {
9551              case 27:    // Escape key
9552              case 40:    // DOWN
9553                CloseInternalViewer(G.VOM.currItemIdx);
9554                break;
9555              case 32:    // SPACE
9556              case 13:    // ENTER
9557                SlideshowToggle();
9558                break;
9559              case 38:    // UP
9560              case 39:    // RIGHT
9561              case 33:    // PAGE UP
9562                DisplayNextMedia();
9563                break;
9564              case 37:    // LEFT
9565              case 34:    // PAGE DOWN
9566                DisplayPreviousMedia();
9567                break;
9568              case 35:    // END
9569              case 36:    // BEGIN
9570            }
9571          }
9572        }
9573      });
9574      
9575      // mouse wheel to zoom in/out the image displayed in the internal lightbox
9576      jQuery(window).bind('mousewheel wheel', function(e){
9577        if( G.VOM.viewerDisplayed ) {
9578          var deltaY = 0;
9579          e.preventDefault();
9580
9581          if( ViewerZoomStart() ) {
9582            if (e.originalEvent.deltaY) { // FireFox 17+ (IE9+, Chrome 31+?)
9583              deltaY = e.originalEvent.deltaY;
9584            } else if (e.originalEvent.wheelDelta) {
9585              deltaY = -e.originalEvent.wheelDelta;
9586            }
9587            ViewerZoomIn( deltaY <= 0 ? true : false );
9588          }
9589        }
9590      });
9591      
9592      // mouse mouse -> unhide lightbox toolbars
9593      jQuery(window).bind('mousemove', function(e){
9594        if( G.VOM.viewerDisplayed ) {
9595          debounce( ViewerToolsUnHide, 100, false )();
9596        }
9597      });
9598      
9599      // fullscreen mode on/off --> internal lightbox
9600      if( ngscreenfull.enabled ) {
9601        // ngscreenfull.onchange(() => {
9602        ngscreenfull.onchange( function() {
9603          if( G.VOM.viewerDisplayed ) {
9604            if( ngscreenfull.isFullscreen ) {
9605              G.VOM.viewerIsFullscreen=true;
9606              G.VOM.$viewer.find('.fullscreenButton').html(G.O.icons.viewerFullscreenOff);
9607            }
9608            else {
9609              G.VOM.viewerIsFullscreen=false;
9610              G.VOM.$viewer.find('.fullscreenButton').html(G.O.icons.viewerFullscreenOn);
9611            }
9612          }
9613        });
9614      }
9615
9616    }
9617    
9618    //----- Manage browser location hash (deep linking and browser back/forward)
9619    function ProcessLocationHash() {
9620
9621      // standard use case -> location hash processing
9622      if( !G.O.locationHash ) { return false; }
9623
9624      var curGal = '#nanogallery/' + G.baseEltID + '/',
9625      newLocationHash = location.hash;
9626      if( G.O.debugMode ) {
9627        console.log('------------------------ PROCESS LOCATION HASH');
9628        console.log('newLocationHash1: ' +newLocationHash);
9629        console.log('G.locationHashLastUsed: ' +G.locationHashLastUsed);
9630      }
9631      
9632      if( newLocationHash == '' ) {
9633        // if( G.GOM.lastDisplayedIdx != -1 ) {
9634        if( G.locationHashLastUsed !== '' ) {
9635          // back button and no hash --> display first album
9636          if( G.O.debugMode ) { console.log('display root album'  ); }
9637          G.locationHashLastUsed = '';
9638          if( G.O.debugMode ) { console.log('new3 G.locationHashLastUsed: ' + G.locationHashLastUsed); }
9639          DisplayAlbum('', '0');
9640          return true;
9641        }
9642      }
9643
9644      if( newLocationHash == G.locationHashLastUsed ) { return; }
9645      
9646      if( newLocationHash.indexOf(curGal) == 0 ) {
9647        // item IDs detected
9648        var IDs=parseIDs( newLocationHash.substring(curGal.length) );
9649        if( IDs.imageID != '0' ) {
9650          if( G.O.debugMode ) { console.log('display image: ' + IDs.albumID +'-'+ IDs.imageID ); }
9651          DisplayPhoto( IDs.imageID, IDs.albumID );
9652          return true;
9653        }
9654        else {
9655          if( G.O.debugMode ) { console.log('display album: ' + IDs.albumID  ); }
9656          DisplayAlbum( '-1', IDs.albumID );
9657          return true;
9658        }
9659      }    
9660      
9661      return false;
9662    }
9663
9664    //---- Set a new browser location hash
9665    function SetLocationHash(albumID, imageID ) {
9666      if( !G.O.locationHash ) { return false; }
9667
9668      if( G.O.debugMode ) {
9669        console.log('------------------------ SET LOCATION HASH');
9670      }
9671      
9672      if( imageID == '' && (albumID == '-1' || albumID == '0' || G.O.album == albumID ) ) {
9673        // root album level --> do not set top.location.hash if not already set
9674        if( location.hash != '' ) {
9675          // try to clear the hash if set
9676          if ("pushState" in history) {
9677            history.pushState("", document.title, window.location.pathname + window.location.search);
9678          }
9679          else {
9680            location.hash='';
9681          }
9682        }
9683        G.locationHashLastUsed='';
9684        if( G.O.debugMode ) { console.log('new2 G.locationHashLastUsed: '+G.locationHashLastUsed); }
9685        return;
9686      }
9687      
9688      var newLocationHash='#'+'nanogallery/'+G.baseEltID+'/'+ albumID;
9689      if( imageID != '' ) {
9690        newLocationHash+='/'+imageID;
9691      }
9692
9693      var lH=location.hash;
9694      if( G.O.debugMode ) {
9695        console.log('newLocationHash2: '+newLocationHash);
9696        console.log('location.hash: '+lH);
9697      }
9698
9699      G.locationHashLastUsed=newLocationHash;
9700      if( G.O.debugMode ) { console.log('new G.locationHashLastUsed: '+G.locationHashLastUsed); }
9701      
9702      if(  lH == '' || lH != newLocationHash ) {
9703        // G.locationHashLastUsed='#'+newLocationHash;
9704        try {
9705          top.location.hash=newLocationHash;
9706        }
9707        catch(e) {
9708          // location hash is not supported by current browser --> disable the option
9709          G.O.locationHash=false;
9710        }
9711      }
9712    }
9713    
9714    
9715    function ResizeWindowEvent() {
9716      CacheViewport();
9717
9718      if( G.VOM.viewerDisplayed ) {
9719        ResizeInternalViewer();
9720      }
9721      else {
9722        if( G.galleryResizeEventEnabled ) {
9723          var nw = RetrieveCurWidth();
9724          if( G.GOM.albumIdx != -1 && 
9725                ( G.tn.settings.height[G.GOM.curNavLevel][G.GOM.curWidth] != G.tn.settings.height[G.GOM.curNavLevel][nw] || 
9726                G.tn.settings.width[G.GOM.curNavLevel][G.GOM.curWidth] != G.tn.settings.width[G.GOM.curNavLevel][nw] ) ) {
9727                  // do not use settings.getH() / settings.getW()
9728            // thumbnail size changed --> render the gallery with the new sizes
9729            G.GOM.curWidth = nw;
9730            //G.layout.SetEngine();
9731            G.GOM.pagination.currentPage = 0;
9732            GalleryRender( G.GOM.albumIdx );
9733          }
9734          else {
9735            GalleryResize();
9736          }
9737        }
9738      }
9739    }
9740    
9741     
9742    // Depreciated - if gallery currently refreshed (G.galleryResizeEventEnabled=false), page may be scrolled but it will not be refreshed again
9743    function OnScrollEvent_OLD() {
9744      if( !G.VOM.viewerDisplayed ) {
9745        if( G.galleryResizeEventEnabled ) {
9746          GalleryResize();
9747        }
9748        return;
9749      }
9750    }
9751
9752
9753    function OnScrollEvent() {
9754      if( !G.VOM.viewerDisplayed ) {
9755        GalleryResizeOnScrollEvent();
9756      }
9757    }
vendor: 17,549 bytes, lines 9757-10364
9757
9758    // 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
9759    function GalleryResizeOnScrollEvent() {
9760      if( G.galleryResizeEventEnabled == false) {
9761        window.setTimeout(GalleryResizeOnScrollEvent, 10);  // check again in 10ms
9762      } else {
9763        GalleryResize();
9764      }
9765    }
9766
9767
9768    
9769    // I18N : define text translations
9770    function i18n() {
9771
9772      // browser language
9773      G.i18nLang = (navigator.language || navigator.userLanguage).toUpperCase();
9774      if( G.i18nLang === 'UNDEFINED') { G.i18nLang=''; }
9775
9776      var llang=-('_'+G.i18nLang).length;
9777      
9778      if( toType(G.O.i18n) == 'object' ){
9779      
9780        for( var key in G.O.i18n ) {
9781          //var value = G.O.i18n[key];
9782          var s=key.substr(llang);
9783          if( s == ('_'+G.i18nLang) ) {
9784            G.i18nTranslations[key.substr(0,key.length-s.length)]=G.O.i18n[key];
9785          } 
9786          else {
9787            G.i18nTranslations[key]=G.O.i18n[key];
9788          }
9789        }
9790      }
9791    }
9792
9793    function GetI18nItem( item, property ) {
9794      var s='';
9795      if( G.i18nLang != '' ) {
9796        if( item[property+'_'+G.i18nLang] !== undefined && item[property+'_'+G.i18nLang].length>0 ) {
9797          s=item[property+'_'+G.i18nLang];
9798          return s;
9799        }
9800      }
9801      s=item[property];
9802      return s;
9803    }
9804
9805    
9806    function RetrieveCurWidth() {
9807      var vpW = G.GOM.cache.viewport.w;
9808      
9809      if( G.O.breakpointSizeSM > 0 && vpW < G.O.breakpointSizeSM) { return 'xs'; }
9810      if( G.O.breakpointSizeME > 0 && vpW < G.O.breakpointSizeME) { return 'sm'; }
9811      if( G.O.breakpointSizeLA > 0 && vpW < G.O.breakpointSizeLA) { return 'me'; }
9812      if( G.O.breakpointSizeXL > 0 && vpW < G.O.breakpointSizeXL) { return 'la'; }
9813      
9814      return 'xl';
9815    }
9816
9817    
9818    /** @function browserNotification */
9819    function browserNotification() {
9820      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>';
9821      m    += '&nbsp;&nbsp;&nbsp; <a href="http://www.google.com/chrome/?hl=en-US)">Chrome</a><br>';
9822      m    += '&nbsp;&nbsp;&nbsp; <a href="http://www.mozilla.com/firefox/)">Firefox</a><br>';
9823      m    += '&nbsp;&nbsp;&nbsp; <a href="http://www.microsoft.com/windows/internet-explorer/default.aspx">Internet Explorer</a><br>';
9824      m    += '&nbsp;&nbsp;&nbsp; <a href="http://www.apple.com/safari/download/">Safari</a>';
9825      NanoAlert(G,  m, false);
9826    }
9827
9828    // 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
9829    function FirstSupportedPropertyName(prefixedPropertyNames) {
9830      var tempDiv = document.createElement("div");
9831      for (var i = 0; i < prefixedPropertyNames.length; ++i) {
9832        if (typeof tempDiv.style[prefixedPropertyNames[i]] != 'undefined')
9833          return prefixedPropertyNames[i];
9834      }
9835      return null;
9836    }
9837
9838    
9839    
9840    
9841  }
9842  
9843
9844
9845//##########################################################################################################################
9846//## imagesLoaded ##########################################################################################################
9847//##########################################################################################################################
9848
9849// external module EMBEDED in nanogallery
9850// NGY BUILD:
9851// replace "imagesLoaded" with "ngimagesLoaded"
9852// replace "ImagesLoaded" with "ngImagesLoaded"
9853// replace "EvEmitter" with "ngEvEmitter"
9854// replace "var $ = window.jQuery" with "var $ = jQuery;"
9855// 2x (global.ngEvEmitter and window.ngimagesLoaded = f...)ignore package manager and set browser global
9856
9857
9858
9859/*!
9860 * imagesLoaded PACKAGED v4.1.1
9861 * JavaScript is all like "You images are done yet or what?"
9862 * MIT License
9863 */
9864
9865/**
9866 * EvEmitter v1.0.3
9867 * Lil' event emitter
9868 * MIT License
9869 */
9870
9871 
9872/* jshint unused: true, undef: true, strict: true */
9873
9874( function( global, factory ) {
9875  // universal module definition
9876  /* jshint strict: false */ /* globals define, module, window */
9877//  if ( typeof define == 'function' && define.amd ) {
9878    // AMD - RequireJS
9879//    define( 'ev-emitter/ev-emitter',factory );
9880//  } else if ( typeof module == 'object' && module.exports ) {
9881    // CommonJS - Browserify, Webpack
9882//    module.exports = factory();
9883//  } else {
9884    // Browser globals
9885    global.ngEvEmitter = factory();
9886//  }
9887
9888}( typeof window != 'undefined' ? window : this, function() {
9889
9890
9891
9892function ngEvEmitter() {}
9893
9894var proto = ngEvEmitter.prototype;
9895
9896proto.on = function( eventName, listener ) {
9897  if ( !eventName || !listener ) {
9898    return;
9899  }
9900  // set events hash
9901  var events = this._events = this._events || {};
9902  // set listeners array
9903  var listeners = events[ eventName ] = events[ eventName ] || [];
9904  // only add once
9905  if ( listeners.indexOf( listener ) == -1 ) {
9906    listeners.push( listener );
9907  }
9908
9909  return this;
9910};
9911
9912proto.once = function( eventName, listener ) {
9913  if ( !eventName || !listener ) {
9914    return;
9915  }
9916  // add event
9917  this.on( eventName, listener );
9918  // set once flag
9919  // set onceEvents hash
9920  var onceEvents = this._onceEvents = this._onceEvents || {};
9921  // set onceListeners object
9922  var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || {};
9923  // set flag
9924  onceListeners[ listener ] = true;
9925
9926  return this;
9927};
9928
9929proto.off = function( eventName, listener ) {
9930  var listeners = this._events && this._events[ eventName ];
9931  if ( !listeners || !listeners.length ) {
9932    return;
9933  }
9934  var index = listeners.indexOf( listener );
9935  if ( index != -1 ) {
9936    listeners.splice( index, 1 );
9937  }
9938
9939  return this;
9940};
9941
9942proto.emitEvent = function( eventName, args ) {
9943  var listeners = this._events && this._events[ eventName ];
9944  if ( !listeners || !listeners.length ) {
9945    return;
9946  }
9947  var i = 0;
9948  var listener = listeners[i];
9949  args = args || [];
9950  // once stuff
9951  var onceListeners = this._onceEvents && this._onceEvents[ eventName ];
9952
9953  while ( listener ) {
9954    var isOnce = onceListeners && onceListeners[ listener ];
9955    if ( isOnce ) {
9956      // remove listener
9957      // remove before trigger to prevent recursion
9958      this.off( eventName, listener );
9959      // unset once flag
9960      delete onceListeners[ listener ];
9961    }
9962    // trigger listener
9963    listener.apply( this, args );
9964    // get next listener
9965    i += isOnce ? 0 : 1;
9966    listener = listeners[i];
9967  }
9968
9969  return this;
9970};
9971
9972return ngEvEmitter;
9973
9974}));
9975
9976/*!
9977 * ngimagesLoaded v4.1.1
9978 * JavaScript is all like "You images are done yet or what?"
9979 * MIT License
9980 */
9981
9982( function( window, factory ) { 'use strict';
9983  // universal module definition
9984
9985  /*global define: false, module: false, require: false */
9986
9987//  if ( typeof define == 'function' && define.amd ) {
9988    // AMD
9989//    define( [
9990//      'ev-emitter/ev-emitter'
9991//    ], function( ngEvEmitter ) {
9992//      return factory( window, ngEvEmitter );
9993//    });
9994//  } else if ( typeof module == 'object' && module.exports ) {
9995    // CommonJS
9996//    module.exports = factory(
9997//      window,
9998//      require('ev-emitter')
9999//    );
10000//  } else {
10001    // browser global
10002    window.ngimagesLoaded = factory(
10003      window,
10004      window.ngEvEmitter
10005    );
10006  //}
10007
10008})( window,
10009
10010// --------------------------  factory -------------------------- //
10011
10012function factory( window, ngEvEmitter ) {
10013
10014
10015
10016// var $ = window.jQuery;
10017var $ = jQuery;
10018var console = window.console;
10019
10020// -------------------------- helpers -------------------------- //
10021
10022// extend objects
10023function extend( a, b ) {
10024  for ( var prop in b ) {
10025    a[ prop ] = b[ prop ];
10026  }
10027  return a;
10028}
10029
10030// turn element or nodeList into an array
10031function makeArray( obj ) {
10032  var ary = [];
10033  if ( Array.isArray( obj ) ) {
10034    // use object if already an array
10035    ary = obj;
10036  } else if ( typeof obj.length == 'number' ) {
10037    // convert nodeList to array
10038    for ( var i=0; i < obj.length; i++ ) {
10039      ary.push( obj[i] );
10040    }
10041  } else {
10042    // array of single index
10043    ary.push( obj );
10044  }
10045  return ary;
10046}
10047
10048// -------------------------- ngimagesLoaded -------------------------- //
10049
10050/**
10051 * @param {Array, Element, NodeList, String} elem
10052 * @param {Object or Function} options - if function, use as callback
10053 * @param {Function} onAlways - callback function
10054 */
10055function ngImagesLoaded( elem, options, onAlways ) {
10056  // coerce ngImagesLoaded() without new, to be new ngImagesLoaded()
10057  if ( !( this instanceof ngImagesLoaded ) ) {
10058    return new ngImagesLoaded( elem, options, onAlways );
10059  }
10060  // use elem as selector string
10061  if ( typeof elem == 'string' ) {
10062    elem = document.querySelectorAll( elem );
10063  }
10064
10065  this.elements = makeArray( elem );
10066  this.options = extend( {}, this.options );
10067
10068  if ( typeof options == 'function' ) {
10069    onAlways = options;
10070  } else {
10071    extend( this.options, options );
10072  }
10073
10074  if ( onAlways ) {
10075    this.on( 'always', onAlways );
10076  }
10077
10078  this.getImages();
10079
10080  if ( $ ) {
10081    // add jQuery Deferred object
10082    this.jqDeferred = new $.Deferred();
10083  }
10084
10085  // HACK check async to allow time to bind listeners
10086  setTimeout( function() {
10087    this.check();
10088  }.bind( this ));
10089}
10090
10091ngImagesLoaded.prototype = Object.create( ngEvEmitter.prototype );
10092
10093ngImagesLoaded.prototype.options = {};
10094
10095ngImagesLoaded.prototype.getImages = function() {
10096  this.images = [];
10097
10098  // filter & find items if we have an item selector
10099  this.elements.forEach( this.addElementImages, this );
10100};
10101
10102/**
10103 * @param {Node} element
10104 */
10105ngImagesLoaded.prototype.addElementImages = function( elem ) {
10106  // filter siblings
10107  if ( elem.nodeName == 'IMG' ) {
10108    this.addImage( elem );
10109  }
10110  // get background image on element
10111  if ( this.options.background === true ) {
10112    this.addElementBackgroundImages( elem );
10113  }
10114
10115  // find children
10116  // no non-element nodes, #143
10117  var nodeType = elem.nodeType;
10118  if ( !nodeType || !elementNodeTypes[ nodeType ] ) {
10119    return;
10120  }
10121  var childImgs = elem.querySelectorAll('img');
10122  // concat childElems to filterFound array
10123  for ( var i=0; i < childImgs.length; i++ ) {
10124    var img = childImgs[i];
10125    this.addImage( img );
10126  }
10127
10128  // get child background images
10129  if ( typeof this.options.background == 'string' ) {
10130    var children = elem.querySelectorAll( this.options.background );
10131    for ( i=0; i < children.length; i++ ) {
10132      var child = children[i];
10133      this.addElementBackgroundImages( child );
10134    }
10135  }
10136};
10137
10138var elementNodeTypes = {
10139  1: true,
10140  9: true,
10141  11: true
10142};
10143
10144ngImagesLoaded.prototype.addElementBackgroundImages = function( elem ) {
10145  var style = getComputedStyle( elem );
10146  if ( !style ) {
10147    // Firefox returns null if in a hidden iframe https://bugzil.la/548397
10148    return;
10149  }
10150  // get url inside url("...")
10151  var reURL = /url\((['"])?(.*?)\1\)/gi;
10152  var matches = reURL.exec( style.backgroundImage );
10153  while ( matches !== null ) {
10154    var url = matches && matches[2];
10155    if ( url ) {
10156      this.addBackground( url, elem );
10157    }
10158    matches = reURL.exec( style.backgroundImage );
10159  }
10160};
10161
10162/**
10163 * @param {Image} img
10164 */
10165ngImagesLoaded.prototype.addImage = function( img ) {
10166  var loadingImage = new LoadingImage( img );
10167  this.images.push( loadingImage );
10168};
10169
10170ngImagesLoaded.prototype.addBackground = function( url, elem ) {
10171  var background = new Background( url, elem );
10172  this.images.push( background );
10173};
10174
10175ngImagesLoaded.prototype.check = function() {
10176  var _this = this;
10177  this.progressedCount = 0;
10178  this.hasAnyBroken = false;
10179  // complete if no images
10180  if ( !this.images.length ) {
10181    this.complete();
10182    return;
10183  }
10184
10185  function onProgress( image, elem, message ) {
10186    // HACK - Chrome triggers event before object properties have changed. #83
10187    setTimeout( function() {
10188      _this.progress( image, elem, message );
10189    });
10190  }
10191
10192  this.images.forEach( function( loadingImage ) {
10193    loadingImage.once( 'progress', onProgress );
10194    loadingImage.check();
10195  });
10196};
10197
10198ngImagesLoaded.prototype.progress = function( image, elem, message ) {
10199  this.progressedCount++;
10200  this.hasAnyBroken = this.hasAnyBroken || !image.isLoaded;
10201  // progress event
10202  this.emitEvent( 'progress', [ this, image, elem ] );
10203  if ( this.jqDeferred && this.jqDeferred.notify ) {
10204    this.jqDeferred.notify( this, image );
10205  }
10206  // check if completed
10207  if ( this.progressedCount == this.images.length ) {
10208    this.complete();
10209  }
10210
10211  if ( this.options.debug && console ) {
10212    console.log( 'progress: ' + message, image, elem );
10213  }
10214};
10215
10216ngImagesLoaded.prototype.complete = function() {
10217  var eventName = this.hasAnyBroken ? 'fail' : 'done';
10218  this.isComplete = true;
10219  this.emitEvent( eventName, [ this ] );
10220  this.emitEvent( 'always', [ this ] );
10221  if ( this.jqDeferred ) {
10222    var jqMethod = this.hasAnyBroken ? 'reject' : 'resolve';
10223    this.jqDeferred[ jqMethod ]( this );
10224  }
10225};
10226
10227// --------------------------  -------------------------- //
10228
10229function LoadingImage( img ) {
10230  this.img = img;
10231}
10232
10233LoadingImage.prototype = Object.create( ngEvEmitter.prototype );
10234
10235LoadingImage.prototype.check = function() {
10236  // If complete is true and browser supports natural sizes,
10237  // try to check for image status manually.
10238  var isComplete = this.getIsImageComplete();
10239  if ( isComplete ) {
10240    // report based on naturalWidth
10241    this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' );
10242    return;
10243  }
10244
10245  // If none of the checks above matched, simulate loading on detached element.
10246  this.proxyImage = new Image();
10247  this.proxyImage.addEventListener( 'load', this );
10248  this.proxyImage.addEventListener( 'error', this );
10249  // bind to image as well for Firefox. #191
10250  this.img.addEventListener( 'load', this );
10251  this.img.addEventListener( 'error', this );
10252  this.proxyImage.src = this.img.src;
10253};
10254
10255LoadingImage.prototype.getIsImageComplete = function() {
10256  return this.img.complete && this.img.naturalWidth !== undefined;
10257};
10258
10259LoadingImage.prototype.confirm = function( isLoaded, message ) {
10260  this.isLoaded = isLoaded;
10261  this.emitEvent( 'progress', [ this, this.img, message ] );
10262};
10263
10264// ----- events ----- //
10265
10266// trigger specified handler for event type
10267LoadingImage.prototype.handleEvent = function( event ) {
10268  var method = 'on' + event.type;
10269  if ( this[ method ] ) {
10270    this[ method ]( event );
10271  }
10272};
10273
10274LoadingImage.prototype.onload = function() {
10275  this.confirm( true, 'onload' );
10276  this.unbindEvents();
10277};
10278
10279LoadingImage.prototype.onerror = function() {
10280  this.confirm( false, 'onerror' );
10281  this.unbindEvents();
10282};
10283
10284LoadingImage.prototype.unbindEvents = function() {
10285  this.proxyImage.removeEventListener( 'load', this );
10286  this.proxyImage.removeEventListener( 'error', this );
10287  this.img.removeEventListener( 'load', this );
10288  this.img.removeEventListener( 'error', this );
10289};
10290
10291// -------------------------- Background -------------------------- //
10292
10293function Background( url, element ) {
10294  this.url = url;
10295  this.element = element;
10296  this.img = new Image();
10297}
10298
10299// inherit LoadingImage prototype
10300Background.prototype = Object.create( LoadingImage.prototype );
10301
10302Background.prototype.check = function() {
10303  this.img.addEventListener( 'load', this );
10304  this.img.addEventListener( 'error', this );
10305  this.img.src = this.url;
10306  // check if image is already complete
10307  var isComplete = this.getIsImageComplete();
10308  if ( isComplete ) {
10309    this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' );
10310    this.unbindEvents();
10311  }
10312};
10313
10314Background.prototype.unbindEvents = function() {
10315  this.img.removeEventListener( 'load', this );
10316  this.img.removeEventListener( 'error', this );
10317};
10318
10319Background.prototype.confirm = function( isLoaded, message ) {
10320  this.isLoaded = isLoaded;
10321  this.emitEvent( 'progress', [ this, this.element, message ] );
10322};
10323
10324// -------------------------- jQuery -------------------------- //
10325
10326ngImagesLoaded.makeJQueryPlugin = function( jQuery ) {
10327  jQuery = jQuery || window.jQuery;
10328  if ( !jQuery ) {
10329    return;
10330  }
10331  // set local variable
10332  $ = jQuery;
10333  // $().ngimagesLoaded()
10334  $.fn.ngimagesLoaded = function( options, callback ) {
10335    var instance = new ngImagesLoaded( this, options, callback );
10336    return instance.jqDeferred.promise( $(this) );
10337  };
10338};
10339// try making plugin
10340ngImagesLoaded.makeJQueryPlugin();
10341
10342// --------------------------  -------------------------- //
10343
10344return ngImagesLoaded;
10345
10346});
10347
10348
10349
10350//##########################################################################################################################
10351//## screenfull.js #########################################################################################################
10352//##########################################################################################################################
10353
10354// screenfull.js
10355// v4.0.1
10356// by sindresorhus - https://github.com/sindresorhus
10357// from: https://github.com/sindresorhus/screenfull.js
10358
10359// external module embeded in nanogallery
10360// NGY BUILD:
10361// replace "screenfull" with "ngscreenfull"
10362// 
10363
10364(function () {
vendor: 5,376 bytes, lines 10365-10565
10365  'use strict';
10366
10367  var document = typeof window !== 'undefined' && typeof window.document !== 'undefined' ? window.document : {};
10368  var isCommonjs = typeof module !== 'undefined' && module.exports;
10369  var keyboardAllowed = typeof Element !== 'undefined' && 'ALLOW_KEYBOARD_INPUT' in Element;
10370
10371  var fn = (function () {
10372    var val;
10373
10374    var fnMap = [
10375      [
10376        'requestFullscreen',
10377        'exitFullscreen',
10378        'fullscreenElement',
10379        'fullscreenEnabled',
10380        'fullscreenchange',
10381        'fullscreenerror'
10382      ],
10383      // New WebKit
10384      [
10385        'webkitRequestFullscreen',
10386        'webkitExitFullscreen',
10387        'webkitFullscreenElement',
10388        'webkitFullscreenEnabled',
10389        'webkitfullscreenchange',
10390        'webkitfullscreenerror'
10391
10392      ],
10393      // Old WebKit (Safari 5.1)
10394      [
10395        'webkitRequestFullScreen',
10396        'webkitCancelFullScreen',
10397        'webkitCurrentFullScreenElement',
10398        'webkitCancelFullScreen',
10399        'webkitfullscreenchange',
10400        'webkitfullscreenerror'
10401
10402      ],
10403      [
10404        'mozRequestFullScreen',
10405        'mozCancelFullScreen',
10406        'mozFullScreenElement',
10407        'mozFullScreenEnabled',
10408        'mozfullscreenchange',
10409        'mozfullscreenerror'
10410      ],
10411      [
10412        'msRequestFullscreen',
10413        'msExitFullscreen',
10414        'msFullscreenElement',
10415        'msFullscreenEnabled',
10416        'MSFullscreenChange',
10417        'MSFullscreenError'
10418      ]
10419    ];
10420
10421    var i = 0;
10422    var l = fnMap.length;
10423    var ret = {};
10424
10425    for (; i < l; i++) {
10426      val = fnMap[i];
10427      if (val && val[1] in document) {
10428        for (i = 0; i < val.length; i++) {
10429          ret[fnMap[0][i]] = val[i];
10430        }
10431        return ret;
10432      }
10433    }
10434
10435    return false;
10436  })();
10437
10438  var eventNameMap = {
10439    change: fn.fullscreenchange,
10440    error: fn.fullscreenerror
10441  };
10442
10443  var ngscreenfull = {
10444    request: function (elem) {
10445      return new Promise(function (resolve) {
10446        var request = fn.requestFullscreen;
10447
10448        var onFullScreenEntered = function () {
10449          this.off('change', onFullScreenEntered);
10450          resolve();
10451        }.bind(this);
10452
10453        elem = elem || document.documentElement;
10454
10455        // Work around Safari 5.1 bug: reports support for
10456        // keyboard in fullscreen even though it doesn't.
10457        // Browser sniffing, since the alternative with
10458        // setTimeout is even worse.
10459        if (/ Version\/5\.1(?:\.\d+)? Safari\//.test(navigator.userAgent)) {
10460          elem[request]();
10461        } else {
10462          elem[request](keyboardAllowed ? Element.ALLOW_KEYBOARD_INPUT : {});
10463        }
10464
10465        this.on('change', onFullScreenEntered);
10466      }.bind(this));
10467    },
10468    exit: function () {
10469      return new Promise(function (resolve) {
10470        if (!this.isFullscreen) {
10471          resolve();
10472          return;
10473        }
10474
10475        var onFullScreenExit = function () {
10476          this.off('change', onFullScreenExit);
10477          resolve();
10478        }.bind(this);
10479
10480        document[fn.exitFullscreen]();
10481
10482        this.on('change', onFullScreenExit);
10483      }.bind(this));
10484    },
10485    toggle: function (elem) {
10486      return this.isFullscreen ? this.exit() : this.request(elem);
10487    },
10488    onchange: function (callback) {
10489      this.on('change', callback);
10490    },
10491    onerror: function (callback) {
10492      this.on('error', callback);
10493    },
10494    on: function (event, callback) {
10495      var eventName = eventNameMap[event];
10496      if (eventName) {
10497        document.addEventListener(eventName, callback, false);
10498      }
10499    },
10500    off: function (event, callback) {
10501      var eventName = eventNameMap[event];
10502      if (eventName) {
10503        document.removeEventListener(eventName, callback, false);
10504      }
10505    },
10506    raw: fn
10507  };
10508
10509  if (!fn) {
10510    if (isCommonjs) {
10511      module.exports = false;
10512    } else {
10513      window.ngscreenfull = false;
10514    }
10515
10516    return;
10517  }
10518
10519  Object.defineProperties(ngscreenfull, {
10520    isFullscreen: {
10521      get: function () {
10522        return Boolean(document[fn.fullscreenElement]);
10523      }
10524    },
10525    element: {
10526      enumerable: true,
10527      get: function () {
10528        return document[fn.fullscreenElement];
10529      }
10530    },
10531    enabled: {
10532      enumerable: true,
10533      get: function () {
10534        // Coerce to boolean in case of old WebKit
10535        return Boolean(document[fn.fullscreenEnabled]);
10536      }
10537    }
10538  });
10539
10540  if (isCommonjs) {
10541    module.exports = ngscreenfull;
10542  } else {
10543    window.ngscreenfull = ngscreenfull;
10544  }
10545})();
10546
10547
10548  
10549//##########################################################################################################################
10550//## Shifty ################################################################################################################
10551//##########################################################################################################################
10552  
10553 /*!
10554 * Shifty
10555 * By Jeremy Kahn - [email protected]
10556 */
10557
10558// external module EMBEDED in nanogallery
10559// NGY BUILD:
10560// 
10561// replace "Tweenable" with "NGTweenable"
10562// replace "define.amd" with "define.amdDISABLED"
10563/* shifty - v1.5.3 - 2016-11-29 - http://jeremyckahn.github.io/shifty */
10564;(function () {
10565  var root = this || Function('return this')();
vendor: 129,699 bytes, lines 10565-14895
10565
10566
10567/**
10568 * Shifty Core
10569 * By Jeremy Kahn - [email protected]
10570 */
10571
10572var NGTweenable = (function () {
10573
10574  'use strict';
10575
10576  // Aliases that get defined later in this function
10577  var formula;
10578
10579  // CONSTANTS
10580  var DEFAULT_SCHEDULE_FUNCTION;
10581  var DEFAULT_EASING = 'linear';
10582  var DEFAULT_DURATION = 500;
10583  var UPDATE_TIME = 1000 / 60;
10584
10585  var _now = Date.now
10586       ? Date.now
10587       : function () {return +new Date();};
10588
10589  var now = typeof SHIFTY_DEBUG_NOW !== 'undefined' ? SHIFTY_DEBUG_NOW : _now;
10590
10591  if (typeof window !== 'undefined') {
10592    // requestAnimationFrame() shim by Paul Irish (modified for Shifty)
10593    // http://paulirish.com/2011/requestanimationframe-for-smart-animating/
10594    DEFAULT_SCHEDULE_FUNCTION = window.requestAnimationFrame
10595       || window.webkitRequestAnimationFrame
10596       || window.oRequestAnimationFrame
10597       || window.msRequestAnimationFrame
10598       || (window.mozCancelRequestAnimationFrame
10599       && window.mozRequestAnimationFrame)
10600       || setTimeout;
10601  } else {
10602    DEFAULT_SCHEDULE_FUNCTION = setTimeout;
10603  }
10604
10605  function noop () {
10606    // NOOP!
10607  }
10608
10609  /**
10610   * Handy shortcut for doing a for-in loop. This is not a "normal" each
10611   * function, it is optimized for Shifty.  The iterator function only receives
10612   * the property name, not the value.
10613   * @param {Object} obj
10614   * @param {Function(string)} fn
10615   * @private
10616   */
10617  function each (obj, fn) {
10618    var key;
10619    for (key in obj) {
10620      if (Object.hasOwnProperty.call(obj, key)) {
10621        fn(key);
10622      }
10623    }
10624  }
10625
10626  /**
10627   * Perform a shallow copy of Object properties.
10628   * @param {Object} targetObject The object to copy into
10629   * @param {Object} srcObject The object to copy from
10630   * @return {Object} A reference to the augmented `targetObj` Object
10631   * @private
10632   */
10633  function shallowCopy (targetObj, srcObj) {
10634    each(srcObj, function (prop) {
10635      targetObj[prop] = srcObj[prop];
10636    });
10637
10638    return targetObj;
10639  }
10640
10641  /**
10642   * Copies each property from src onto target, but only if the property to
10643   * copy to target is undefined.
10644   * @param {Object} target Missing properties in this Object are filled in
10645   * @param {Object} src
10646   * @private
10647   */
10648  function defaults (target, src) {
10649    each(src, function (prop) {
10650      if (typeof target[prop] === 'undefined') {
10651        target[prop] = src[prop];
10652      }
10653    });
10654  }
10655
10656  /**
10657   * Calculates the interpolated tween values of an Object for a given
10658   * timestamp.
10659   * @param {Number} forPosition The position to compute the state for.
10660   * @param {Object} currentState Current state properties.
10661   * @param {Object} originalState: The original state properties the Object is
10662   * tweening from.
10663   * @param {Object} targetState: The destination state properties the Object
10664   * is tweening to.
10665   * @param {number} duration: The length of the tween in milliseconds.
10666   * @param {number} timestamp: The UNIX epoch time at which the tween began.
10667   * @param {Object} easing: This Object's keys must correspond to the keys in
10668   * targetState.
10669   * @private
10670   */
10671  function tweenProps (forPosition, currentState, originalState, targetState,
10672    duration, timestamp, easing) {
10673    var normalizedPosition =
10674        forPosition < timestamp ? 0 : (forPosition - timestamp) / duration;
10675
10676
10677    var prop;
10678    var easingObjectProp;
10679    var easingFn;
10680    for (prop in currentState) {
10681      if (currentState.hasOwnProperty(prop)) {
10682        easingObjectProp = easing[prop];
10683        easingFn = typeof easingObjectProp === 'function'
10684          ? easingObjectProp
10685          : formula[easingObjectProp];
10686
10687        currentState[prop] = tweenProp(
10688          originalState[prop],
10689          targetState[prop],
10690          easingFn,
10691          normalizedPosition
10692        );
10693      }
10694    }
10695
10696    return currentState;
10697  }
10698
10699  /**
10700   * Tweens a single property.
10701   * @param {number} start The value that the tween started from.
10702   * @param {number} end The value that the tween should end at.
10703   * @param {Function} easingFunc The easing curve to apply to the tween.
10704   * @param {number} position The normalized position (between 0.0 and 1.0) to
10705   * calculate the midpoint of 'start' and 'end' against.
10706   * @return {number} The tweened value.
10707   * @private
10708   */
10709  function tweenProp (start, end, easingFunc, position) {
10710    return start + (end - start) * easingFunc(position);
10711  }
10712
10713  /**
10714   * Applies a filter to NGTweenable instance.
10715   * @param {NGTweenable} tweenable The `NGTweenable` instance to call the filter
10716   * upon.
10717   * @param {String} filterName The name of the filter to apply.
10718   * @private
10719   */
10720  function applyFilter (tweenable, filterName) {
10721    var filters = NGTweenable.prototype.filter;
10722    var args = tweenable._filterArgs;
10723
10724    each(filters, function (name) {
10725      if (typeof filters[name][filterName] !== 'undefined') {
10726        filters[name][filterName].apply(tweenable, args);
10727      }
10728    });
10729  }
10730
10731  var timeoutHandler_endTime;
10732  var timeoutHandler_currentTime;
10733  var timeoutHandler_isEnded;
10734  var timeoutHandler_offset;
10735  /**
10736   * Handles the update logic for one step of a tween.
10737   * @param {NGTweenable} tweenable
10738   * @param {number} timestamp
10739   * @param {number} delay
10740   * @param {number} duration
10741   * @param {Object} currentState
10742   * @param {Object} originalState
10743   * @param {Object} targetState
10744   * @param {Object} easing
10745   * @param {Function(Object, *, number)} step
10746   * @param {Function(Function,number)}} schedule
10747   * @param {number=} opt_currentTimeOverride Needed for accurate timestamp in
10748   * NGTweenable#seek.
10749   * @private
10750   */
10751  function timeoutHandler (tweenable, timestamp, delay, duration, currentState,
10752    originalState, targetState, easing, step, schedule,
10753    opt_currentTimeOverride) {
10754
10755    timeoutHandler_endTime = timestamp + delay + duration;
10756
10757    timeoutHandler_currentTime =
10758    Math.min(opt_currentTimeOverride || now(), timeoutHandler_endTime);
10759
10760    timeoutHandler_isEnded =
10761      timeoutHandler_currentTime >= timeoutHandler_endTime;
10762
10763    timeoutHandler_offset = duration - (
10764      timeoutHandler_endTime - timeoutHandler_currentTime);
10765
10766    if (tweenable.isPlaying()) {
10767      if (timeoutHandler_isEnded) {
10768        step(targetState, tweenable._attachment, timeoutHandler_offset);
10769        tweenable.stop(true);
10770      } else {
10771        tweenable._scheduleId =
10772          schedule(tweenable._timeoutHandler, UPDATE_TIME);
10773
10774        applyFilter(tweenable, 'beforeTween');
10775
10776        // If the animation has not yet reached the start point (e.g., there was
10777        // delay that has not yet completed), just interpolate the starting
10778        // position of the tween.
10779        if (timeoutHandler_currentTime < (timestamp + delay)) {
10780          tweenProps(1, currentState, originalState, targetState, 1, 1, easing);
10781        } else {
10782          tweenProps(timeoutHandler_currentTime, currentState, originalState,
10783            targetState, duration, timestamp + delay, easing);
10784        }
10785
10786        applyFilter(tweenable, 'afterTween');
10787
10788        step(currentState, tweenable._attachment, timeoutHandler_offset);
10789      }
10790    }
10791  }
10792
10793
10794  /**
10795   * Creates a usable easing Object from a string, a function or another easing
10796   * Object.  If `easing` is an Object, then this function clones it and fills
10797   * in the missing properties with `"linear"`.
10798   * @param {Object.<string|Function>} fromTweenParams
10799   * @param {Object|string|Function} easing
10800   * @return {Object.<string|Function>}
10801   * @private
10802   */
10803  function composeEasingObject (fromTweenParams, easing) {
10804    var composedEasing = {};
10805    var typeofEasing = typeof easing;
10806
10807    if (typeofEasing === 'string' || typeofEasing === 'function') {
10808      each(fromTweenParams, function (prop) {
10809        composedEasing[prop] = easing;
10810      });
10811    } else {
10812      each(fromTweenParams, function (prop) {
10813        if (!composedEasing[prop]) {
10814          composedEasing[prop] = easing[prop] || DEFAULT_EASING;
10815        }
10816      });
10817    }
10818
10819    return composedEasing;
10820  }
10821
10822  /**
10823   * NGTweenable constructor.
10824   * @class NGTweenable
10825   * @param {Object=} opt_initialState The values that the initial tween should
10826   * start at if a `from` object is not provided to `{{#crossLink
10827   * "NGTweenable/tween:method"}}{{/crossLink}}` or `{{#crossLink
10828   * "NGTweenable/setConfig:method"}}{{/crossLink}}`.
10829   * @param {Object=} opt_config Configuration object to be passed to
10830   * `{{#crossLink "NGTweenable/setConfig:method"}}{{/crossLink}}`.
10831   * @module NGTweenable
10832   * @constructor
10833   */
10834  function NGTweenable (opt_initialState, opt_config) {
10835    this._currentState = opt_initialState || {};
10836    this._configured = false;
10837    this._scheduleFunction = DEFAULT_SCHEDULE_FUNCTION;
10838
10839    // To prevent unnecessary calls to setConfig do not set default
10840    // configuration here.  Only set default configuration immediately before
10841    // tweening if none has been set.
10842    if (typeof opt_config !== 'undefined') {
10843      this.setConfig(opt_config);
10844    }
10845  }
10846
10847  /**
10848   * Configure and start a tween.
10849   * @method tween
10850   * @param {Object=} opt_config Configuration object to be passed to
10851   * `{{#crossLink "NGTweenable/setConfig:method"}}{{/crossLink}}`.
10852   * @chainable
10853   */
10854  NGTweenable.prototype.tween = function (opt_config) {
10855    if (this._isTweening) {
10856      return this;
10857    }
10858
10859    // Only set default config if no configuration has been set previously and
10860    // none is provided now.
10861    if (opt_config !== undefined || !this._configured) {
10862      this.setConfig(opt_config);
10863    }
10864
10865    this._timestamp = now();
10866    this._start(this.get(), this._attachment);
10867    return this.resume();
10868  };
10869
10870  /**
10871   * Configure a tween that will start at some point in the future.
10872   *
10873   * @method setConfig
10874   * @param {Object} config The following values are valid:
10875   * - __from__ (_Object=_): Starting position.  If omitted, `{{#crossLink
10876   *   "NGTweenable/get:method"}}get(){{/crossLink}}` is used.
10877   * - __to__ (_Object=_): Ending position.
10878   * - __duration__ (_number=_): How many milliseconds to animate for.
10879   * - __delay__ (_delay=_): How many milliseconds to wait before starting the
10880   *   tween.
10881   * - __start__ (_Function(Object, *)_): Function to execute when the tween
10882   *   begins.  Receives the state of the tween as the first parameter and
10883   *   `attachment` as the second parameter.
10884   * - __step__ (_Function(Object, *, number)_): Function to execute on every
10885   *   tick.  Receives `{{#crossLink
10886   *   "NGTweenable/get:method"}}get(){{/crossLink}}` as the first parameter,
10887   *   `attachment` as the second parameter, and the time elapsed since the
10888   *   start of the tween as the third. This function is not called on the
10889   *   final step of the animation, but `finish` is.
10890   * - __finish__ (_Function(Object, *)_): Function to execute upon tween
10891   *   completion.  Receives the state of the tween as the first parameter and
10892   *   `attachment` as the second parameter.
10893   * - __easing__ (_Object.<string|Function>|string|Function=_): Easing curve
10894   *   name(s) or function(s) to use for the tween.
10895   * - __attachment__ (_*_): Cached value that is passed to the
10896   *   `step`/`start`/`finish` methods.
10897   * @chainable
10898   */
10899  NGTweenable.prototype.setConfig = function (config) {
10900    config = config || {};
10901    this._configured = true;
10902
10903    // Attach something to this NGTweenable instance (e.g.: a DOM element, an
10904    // object, a string, etc.);
10905    this._attachment = config.attachment;
10906
10907    // Init the internal state
10908    this._pausedAtTime = null;
10909    this._scheduleId = null;
10910    this._delay = config.delay || 0;
10911    this._start = config.start || noop;
10912    this._step = config.step || noop;
10913    this._finish = config.finish || noop;
10914    this._duration = config.duration || DEFAULT_DURATION;
10915    this._currentState = shallowCopy({}, config.from || this.get());
10916    this._originalState = this.get();
10917    this._targetState = shallowCopy({}, config.to || this.get());
10918
10919    var self = this;
10920    this._timeoutHandler = function () {
10921      timeoutHandler(self,
10922        self._timestamp,
10923        self._delay,
10924        self._duration,
10925        self._currentState,
10926        self._originalState,
10927        self._targetState,
10928        self._easing,
10929        self._step,
10930        self._scheduleFunction
10931      );
10932    };
10933
10934    // Aliases used below
10935    var currentState = this._currentState;
10936    var targetState = this._targetState;
10937
10938    // Ensure that there is always something to tween to.
10939    defaults(targetState, currentState);
10940
10941    this._easing = composeEasingObject(
10942      currentState, config.easing || DEFAULT_EASING);
10943
10944    this._filterArgs =
10945      [currentState, this._originalState, targetState, this._easing];
10946
10947    applyFilter(this, 'tweenCreated');
10948    return this;
10949  };
10950
10951  /**
10952   * @method get
10953   * @return {Object} The current state.
10954   */
10955  NGTweenable.prototype.get = function () {
10956    return shallowCopy({}, this._currentState);
10957  };
10958
10959  /**
10960   * @method set
10961   * @param {Object} state The current state.
10962   */
10963  NGTweenable.prototype.set = function (state) {
10964    this._currentState = state;
10965  };
10966
10967  /**
10968   * Pause a tween.  Paused tweens can be resumed from the point at which they
10969   * were paused.  This is different from `{{#crossLink
10970   * "NGTweenable/stop:method"}}{{/crossLink}}`, as that method
10971   * causes a tween to start over when it is resumed.
10972   * @method pause
10973   * @chainable
10974   */
10975  NGTweenable.prototype.pause = function () {
10976    this._pausedAtTime = now();
10977    this._isPaused = true;
10978    return this;
10979  };
10980
10981  /**
10982   * Resume a paused tween.
10983   * @method resume
10984   * @chainable
10985   */
10986  NGTweenable.prototype.resume = function () {
10987    if (this._isPaused) {
10988      this._timestamp += now() - this._pausedAtTime;
10989    }
10990
10991    this._isPaused = false;
10992    this._isTweening = true;
10993
10994    this._timeoutHandler();
10995
10996    return this;
10997  };
10998
10999  /**
11000   * Move the state of the animation to a specific point in the tween's
11001   * timeline.  If the animation is not running, this will cause the `step`
11002   * handlers to be called.
11003   * @method seek
11004   * @param {millisecond} millisecond The millisecond of the animation to seek
11005   * to.  This must not be less than `0`.
11006   * @chainable
11007   */
11008  NGTweenable.prototype.seek = function (millisecond) {
11009    millisecond = Math.max(millisecond, 0);
11010    var currentTime = now();
11011
11012    if ((this._timestamp + millisecond) === 0) {
11013      return this;
11014    }
11015
11016    this._timestamp = currentTime - millisecond;
11017
11018    if (!this.isPlaying()) {
11019      this._isTweening = true;
11020      this._isPaused = false;
11021
11022      // If the animation is not running, call timeoutHandler to make sure that
11023      // any step handlers are run.
11024      timeoutHandler(this,
11025        this._timestamp,
11026        this._delay,
11027        this._duration,
11028        this._currentState,
11029        this._originalState,
11030        this._targetState,
11031        this._easing,
11032        this._step,
11033        this._scheduleFunction,
11034        currentTime
11035      );
11036
11037      this.pause();
11038    }
11039
11040    return this;
11041  };
11042
11043  /**
11044   * Stops and cancels a tween.
11045   * @param {boolean=} gotoEnd If `false` or omitted, the tween just stops at
11046   * its current state, and the `finish` handler is not invoked.  If `true`,
11047   * the tweened object's values are instantly set to the target values, and
11048   * `finish` is invoked.
11049   * @method stop
11050   * @chainable
11051   */
11052  NGTweenable.prototype.stop = function (gotoEnd) {
11053    this._isTweening = false;
11054    this._isPaused = false;
11055    this._timeoutHandler = noop;
11056
11057    (root.cancelAnimationFrame            ||
11058    root.webkitCancelAnimationFrame     ||
11059    root.oCancelAnimationFrame          ||
11060    root.msCancelAnimationFrame         ||
11061    root.mozCancelRequestAnimationFrame ||
11062    root.clearTimeout)(this._scheduleId);
11063
11064    if (gotoEnd) {
11065      applyFilter(this, 'beforeTween');
11066      tweenProps(
11067        1,
11068        this._currentState,
11069        this._originalState,
11070        this._targetState,
11071        1,
11072        0,
11073        this._easing
11074      );
11075      applyFilter(this, 'afterTween');
11076      applyFilter(this, 'afterTweenEnd');
11077      this._finish.call(this, this._currentState, this._attachment);
11078    }
11079
11080    return this;
11081  };
11082
11083  /**
11084   * @method isPlaying
11085   * @return {boolean} Whether or not a tween is running.
11086   */
11087  NGTweenable.prototype.isPlaying = function () {
11088    return this._isTweening && !this._isPaused;
11089  };
11090
11091  /**
11092   * Set a custom schedule function.
11093   *
11094   * If a custom function is not set,
11095   * [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame)
11096   * is used if available, otherwise
11097   * [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/Window.setTimeout)
11098   * is used.
11099   * @method setScheduleFunction
11100   * @param {Function(Function,number)} scheduleFunction The function to be
11101   * used to schedule the next frame to be rendered.
11102   */
11103  NGTweenable.prototype.setScheduleFunction = function (scheduleFunction) {
11104    this._scheduleFunction = scheduleFunction;
11105  };
11106
11107  /**
11108   * `delete` all "own" properties.  Call this when the `NGTweenable` instance
11109   * is no longer needed to free memory.
11110   * @method dispose
11111   */
11112  NGTweenable.prototype.dispose = function () {
11113    var prop;
11114    for (prop in this) {
11115      if (this.hasOwnProperty(prop)) {
11116        delete this[prop];
11117      }
11118    }
11119  };
11120
11121  /**
11122   * Filters are used for transforming the properties of a tween at various
11123   * points in a NGTweenable's life cycle.  See the README for more info on this.
11124   * @private
11125   */
11126  NGTweenable.prototype.filter = {};
11127
11128  /**
11129   * This object contains all of the tweens available to Shifty.  It is
11130   * extensible - simply attach properties to the `NGTweenable.prototype.formula`
11131   * Object following the same format as `linear`.
11132   *
11133   * `pos` should be a normalized `number` (between 0 and 1).
11134   * @property formula
11135   * @type {Object(function)}
11136   */
11137  NGTweenable.prototype.formula = {
11138    linear: function (pos) {
11139      return pos;
11140    }
11141  };
11142
11143  formula = NGTweenable.prototype.formula;
11144
11145  shallowCopy(NGTweenable, {
11146    'now': now
11147    ,'each': each
11148    ,'tweenProps': tweenProps
11149    ,'tweenProp': tweenProp
11150    ,'applyFilter': applyFilter
11151    ,'shallowCopy': shallowCopy
11152    ,'defaults': defaults
11153    ,'composeEasingObject': composeEasingObject
11154  });
11155
11156  // `root` is provided in the intro/outro files.
11157
11158  // A hook used for unit testing.
11159  if (typeof SHIFTY_DEBUG_NOW === 'function') {
11160    root.timeoutHandler = timeoutHandler;
11161  }
11162
11163  // Bootstrap NGTweenable appropriately for the environment.
11164  if (typeof exports === 'object') {
11165    // CommonJS
11166    module.exports = NGTweenable;
11167  } else if (typeof define === 'function' && define.amdDISABLED) {
11168    // AMD
11169    define(function () {return NGTweenable;});
11170  } else if (typeof root.NGTweenable === 'undefined') {
11171    // Browser: Make `NGTweenable` globally accessible.
11172    root.NGTweenable = NGTweenable;
11173  }
11174
11175  return NGTweenable;
11176
11177} ());
11178
11179/*!
11180 * All equations are adapted from Thomas Fuchs'
11181 * [Scripty2](https://github.com/madrobby/scripty2/blob/master/src/effects/transitions/penner.js).
11182 *
11183 * Based on Easing Equations (c) 2003 [Robert
11184 * Penner](http://www.robertpenner.com/), all rights reserved. This work is
11185 * [subject to terms](http://www.robertpenner.com/easing_terms_of_use.html).
11186 */
11187
11188/*!
11189 *  TERMS OF USE - EASING EQUATIONS
11190 *  Open source under the BSD License.
11191 *  Easing Equations (c) 2003 Robert Penner, all rights reserved.
11192 */
11193
11194;(function () {
11195
11196  NGTweenable.shallowCopy(NGTweenable.prototype.formula, {
11197    easeInQuad: function (pos) {
11198      return Math.pow(pos, 2);
11199    },
11200
11201    easeOutQuad: function (pos) {
11202      return -(Math.pow((pos - 1), 2) - 1);
11203    },
11204
11205    easeInOutQuad: function (pos) {
11206      if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(pos,2);}
11207      return -0.5 * ((pos -= 2) * pos - 2);
11208    },
11209
11210    easeInCubic: function (pos) {
11211      return Math.pow(pos, 3);
11212    },
11213
11214    easeOutCubic: function (pos) {
11215      return (Math.pow((pos - 1), 3) + 1);
11216    },
11217
11218    easeInOutCubic: function (pos) {
11219      if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(pos,3);}
11220      return 0.5 * (Math.pow((pos - 2),3) + 2);
11221    },
11222
11223    easeInQuart: function (pos) {
11224      return Math.pow(pos, 4);
11225    },
11226
11227    easeOutQuart: function (pos) {
11228      return -(Math.pow((pos - 1), 4) - 1);
11229    },
11230
11231    easeInOutQuart: function (pos) {
11232      if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(pos,4);}
11233      return -0.5 * ((pos -= 2) * Math.pow(pos,3) - 2);
11234    },
11235
11236    easeInQuint: function (pos) {
11237      return Math.pow(pos, 5);
11238    },
11239
11240    easeOutQuint: function (pos) {
11241      return (Math.pow((pos - 1), 5) + 1);
11242    },
11243
11244    easeInOutQuint: function (pos) {
11245      if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(pos,5);}
11246      return 0.5 * (Math.pow((pos - 2),5) + 2);
11247    },
11248
11249    easeInSine: function (pos) {
11250      return -Math.cos(pos * (Math.PI / 2)) + 1;
11251    },
11252
11253    easeOutSine: function (pos) {
11254      return Math.sin(pos * (Math.PI / 2));
11255    },
11256
11257    easeInOutSine: function (pos) {
11258      return (-0.5 * (Math.cos(Math.PI * pos) - 1));
11259    },
11260
11261    easeInExpo: function (pos) {
11262      return (pos === 0) ? 0 : Math.pow(2, 10 * (pos - 1));
11263    },
11264
11265    easeOutExpo: function (pos) {
11266      return (pos === 1) ? 1 : -Math.pow(2, -10 * pos) + 1;
11267    },
11268
11269    easeInOutExpo: function (pos) {
11270      if (pos === 0) {return 0;}
11271      if (pos === 1) {return 1;}
11272      if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(2,10 * (pos - 1));}
11273      return 0.5 * (-Math.pow(2, -10 * --pos) + 2);
11274    },
11275
11276    easeInCirc: function (pos) {
11277      return -(Math.sqrt(1 - (pos * pos)) - 1);
11278    },
11279
11280    easeOutCirc: function (pos) {
11281      return Math.sqrt(1 - Math.pow((pos - 1), 2));
11282    },
11283
11284    easeInOutCirc: function (pos) {
11285      if ((pos /= 0.5) < 1) {return -0.5 * (Math.sqrt(1 - pos * pos) - 1);}
11286      return 0.5 * (Math.sqrt(1 - (pos -= 2) * pos) + 1);
11287    },
11288
11289    easeOutBounce: function (pos) {
11290      if ((pos) < (1 / 2.75)) {
11291        return (7.5625 * pos * pos);
11292      } else if (pos < (2 / 2.75)) {
11293        return (7.5625 * (pos -= (1.5 / 2.75)) * pos + 0.75);
11294      } else if (pos < (2.5 / 2.75)) {
11295        return (7.5625 * (pos -= (2.25 / 2.75)) * pos + 0.9375);
11296      } else {
11297        return (7.5625 * (pos -= (2.625 / 2.75)) * pos + 0.984375);
11298      }
11299    },
11300
11301    easeInBack: function (pos) {
11302      var s = 1.70158;
11303      return (pos) * pos * ((s + 1) * pos - s);
11304    },
11305
11306    easeOutBack: function (pos) {
11307      var s = 1.70158;
11308      return (pos = pos - 1) * pos * ((s + 1) * pos + s) + 1;
11309    },
11310
11311    easeInOutBack: function (pos) {
11312      var s = 1.70158;
11313      if ((pos /= 0.5) < 1) {
11314        return 0.5 * (pos * pos * (((s *= (1.525)) + 1) * pos - s));
11315      }
11316      return 0.5 * ((pos -= 2) * pos * (((s *= (1.525)) + 1) * pos + s) + 2);
11317    },
11318
11319    elastic: function (pos) {
11320      // jshint maxlen:90
11321      return -1 * Math.pow(4,-8 * pos) * Math.sin((pos * 6 - 1) * (2 * Math.PI) / 2) + 1;
11322    },
11323
11324    swingFromTo: function (pos) {
11325      var s = 1.70158;
11326      return ((pos /= 0.5) < 1) ?
11327          0.5 * (pos * pos * (((s *= (1.525)) + 1) * pos - s)) :
11328          0.5 * ((pos -= 2) * pos * (((s *= (1.525)) + 1) * pos + s) + 2);
11329    },
11330
11331    swingFrom: function (pos) {
11332      var s = 1.70158;
11333      return pos * pos * ((s + 1) * pos - s);
11334    },
11335
11336    swingTo: function (pos) {
11337      var s = 1.70158;
11338      return (pos -= 1) * pos * ((s + 1) * pos + s) + 1;
11339    },
11340
11341    bounce: function (pos) {
11342      if (pos < (1 / 2.75)) {
11343        return (7.5625 * pos * pos);
11344      } else if (pos < (2 / 2.75)) {
11345        return (7.5625 * (pos -= (1.5 / 2.75)) * pos + 0.75);
11346      } else if (pos < (2.5 / 2.75)) {
11347        return (7.5625 * (pos -= (2.25 / 2.75)) * pos + 0.9375);
11348      } else {
11349        return (7.5625 * (pos -= (2.625 / 2.75)) * pos + 0.984375);
11350      }
11351    },
11352
11353    bouncePast: function (pos) {
11354      if (pos < (1 / 2.75)) {
11355        return (7.5625 * pos * pos);
11356      } else if (pos < (2 / 2.75)) {
11357        return 2 - (7.5625 * (pos -= (1.5 / 2.75)) * pos + 0.75);
11358      } else if (pos < (2.5 / 2.75)) {
11359        return 2 - (7.5625 * (pos -= (2.25 / 2.75)) * pos + 0.9375);
11360      } else {
11361        return 2 - (7.5625 * (pos -= (2.625 / 2.75)) * pos + 0.984375);
11362      }
11363    },
11364
11365    easeFromTo: function (pos) {
11366      if ((pos /= 0.5) < 1) {return 0.5 * Math.pow(pos,4);}
11367      return -0.5 * ((pos -= 2) * Math.pow(pos,3) - 2);
11368    },
11369
11370    easeFrom: function (pos) {
11371      return Math.pow(pos,4);
11372    },
11373
11374    easeTo: function (pos) {
11375      return Math.pow(pos,0.25);
11376    }
11377  });
11378
11379}());
11380
11381// jshint maxlen:100
11382/**
11383 * The Bezier magic in this file is adapted/copied almost wholesale from
11384 * [Scripty2](https://github.com/madrobby/scripty2/blob/master/src/effects/transitions/cubic-bezier.js),
11385 * which was adapted from Apple code (which probably came from
11386 * [here](http://opensource.apple.com/source/WebCore/WebCore-955.66/platform/graphics/UnitBezier.h)).
11387 * Special thanks to Apple and Thomas Fuchs for much of this code.
11388 */
11389
11390/**
11391 *  Copyright (c) 2006 Apple Computer, Inc. All rights reserved.
11392 *
11393 *  Redistribution and use in source and binary forms, with or without
11394 *  modification, are permitted provided that the following conditions are met:
11395 *
11396 *  1. Redistributions of source code must retain the above copyright notice,
11397 *  this list of conditions and the following disclaimer.
11398 *
11399 *  2. Redistributions in binary form must reproduce the above copyright notice,
11400 *  this list of conditions and the following disclaimer in the documentation
11401 *  and/or other materials provided with the distribution.
11402 *
11403 *  3. Neither the name of the copyright holder(s) nor the names of any
11404 *  contributors may be used to endorse or promote products derived from
11405 *  this software without specific prior written permission.
11406 *
11407 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
11408 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
11409 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
11410 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
11411 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
11412 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
11413 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
11414 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
11415 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
11416 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
11417 *  POSSIBILITY OF SUCH DAMAGE.
11418 */
11419;(function () {
11420  // port of webkit cubic bezier handling by http://www.netzgesta.de/dev/
11421  function cubicBezierAtTime(t,p1x,p1y,p2x,p2y,duration) {
11422    var ax = 0,bx = 0,cx = 0,ay = 0,by = 0,cy = 0;
11423    function sampleCurveX(t) {
11424      return ((ax * t + bx) * t + cx) * t;
11425    }
11426    function sampleCurveY(t) {
11427      return ((ay * t + by) * t + cy) * t;
11428    }
11429    function sampleCurveDerivativeX(t) {
11430      return (3.0 * ax * t + 2.0 * bx) * t + cx;
11431    }
11432    function solveEpsilon(duration) {
11433      return 1.0 / (200.0 * duration);
11434    }
11435    function solve(x,epsilon) {
11436      return sampleCurveY(solveCurveX(x, epsilon));
11437    }
11438    function fabs(n) {
11439      if (n >= 0) {
11440        return n;
11441      } else {
11442        return 0 - n;
11443      }
11444    }
11445    function solveCurveX(x, epsilon) {
11446      var t0,t1,t2,x2,d2,i;
11447      for (t2 = x, i = 0; i < 8; i++) {
11448        x2 = sampleCurveX(t2) - x;
11449        if (fabs(x2) < epsilon) {
11450          return t2;
11451        }
11452        d2 = sampleCurveDerivativeX(t2);
11453        if (fabs(d2) < 1e-6) {
11454          break;
11455        }
11456        t2 = t2 - x2 / d2;
11457      }
11458      t0 = 0.0;
11459      t1 = 1.0;
11460      t2 = x;
11461      if (t2 < t0) {
11462        return t0;
11463      }
11464      if (t2 > t1) {
11465        return t1;
11466      }
11467      while (t0 < t1) {
11468        x2 = sampleCurveX(t2);
11469        if (fabs(x2 - x) < epsilon) {
11470          return t2;
11471        }
11472        if (x > x2) {
11473          t0 = t2;
11474        }else {
11475          t1 = t2;
11476        }
11477        t2 = (t1 - t0) * 0.5 + t0;
11478      }
11479      return t2; // Failure.
11480    }
11481    cx = 3.0 * p1x;
11482    bx = 3.0 * (p2x - p1x) - cx;
11483    ax = 1.0 - cx - bx;
11484    cy = 3.0 * p1y;
11485    by = 3.0 * (p2y - p1y) - cy;
11486    ay = 1.0 - cy - by;
11487    return solve(t, solveEpsilon(duration));
11488  }
11489  /**
11490   *  getCubicBezierTransition(x1, y1, x2, y2) -> Function
11491   *
11492   *  Generates a transition easing function that is compatible
11493   *  with WebKit's CSS transitions `-webkit-transition-timing-function`
11494   *  CSS property.
11495   *
11496   *  The W3C has more information about CSS3 transition timing functions:
11497   *  http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag
11498   *
11499   *  @param {number} x1
11500   *  @param {number} y1
11501   *  @param {number} x2
11502   *  @param {number} y2
11503   *  @return {function}
11504   *  @private
11505   */
11506  function getCubicBezierTransition (x1, y1, x2, y2) {
11507    return function (pos) {
11508      return cubicBezierAtTime(pos,x1,y1,x2,y2,1);
11509    };
11510  }
11511  // End ported code
11512
11513  /**
11514   * Create a Bezier easing function and attach it to `{{#crossLink
11515   * "NGTweenable/formula:property"}}NGTweenable#formula{{/crossLink}}`.  This
11516   * function gives you total control over the easing curve.  Matthew Lein's
11517   * [Ceaser](http://matthewlein.com/ceaser/) is a useful tool for visualizing
11518   * the curves you can make with this function.
11519   * @method setBezierFunction
11520   * @param {string} name The name of the easing curve.  Overwrites the old
11521   * easing function on `{{#crossLink
11522   * "NGTweenable/formula:property"}}NGTweenable#formula{{/crossLink}}` if it
11523   * exists.
11524   * @param {number} x1
11525   * @param {number} y1
11526   * @param {number} x2
11527   * @param {number} y2
11528   * @return {function} The easing function that was attached to
11529   * NGTweenable.prototype.formula.
11530   */
11531  NGTweenable.setBezierFunction = function (name, x1, y1, x2, y2) {
11532    var cubicBezierTransition = getCubicBezierTransition(x1, y1, x2, y2);
11533    cubicBezierTransition.displayName = name;
11534    cubicBezierTransition.x1 = x1;
11535    cubicBezierTransition.y1 = y1;
11536    cubicBezierTransition.x2 = x2;
11537    cubicBezierTransition.y2 = y2;
11538
11539    return NGTweenable.prototype.formula[name] = cubicBezierTransition;
11540  };
11541
11542
11543  /**
11544   * `delete` an easing function from `{{#crossLink
11545   * "NGTweenable/formula:property"}}NGTweenable#formula{{/crossLink}}`.  Be
11546   * careful with this method, as it `delete`s whatever easing formula matches
11547   * `name` (which means you can delete standard Shifty easing functions).
11548   * @method unsetBezierFunction
11549   * @param {string} name The name of the easing function to delete.
11550   * @return {function}
11551   */
11552  NGTweenable.unsetBezierFunction = function (name) {
11553    delete NGTweenable.prototype.formula[name];
11554  };
11555
11556})();
11557
11558;(function () {
11559
11560  function getInterpolatedValues (
11561    from, current, targetState, position, easing, delay) {
11562    return NGTweenable.tweenProps(
11563      position, current, from, targetState, 1, delay, easing);
11564  }
11565
11566  // Fake a NGTweenable and patch some internals.  This approach allows us to
11567  // skip uneccessary processing and object recreation, cutting down on garbage
11568  // collection pauses.
11569  var mockNGTweenable = new NGTweenable();
11570  mockNGTweenable._filterArgs = [];
11571
11572  /**
11573   * Compute the midpoint of two Objects.  This method effectively calculates a
11574   * specific frame of animation that `{{#crossLink
11575   * "NGTweenable/tween:method"}}{{/crossLink}}` does many times over the course
11576   * of a full tween.
11577   *
11578   *     var interpolatedValues = NGTweenable.interpolate({
11579   *       width: '100px',
11580   *       opacity: 0,
11581   *       color: '#fff'
11582   *     }, {
11583   *       width: '200px',
11584   *       opacity: 1,
11585   *       color: '#000'
11586   *     }, 0.5);
11587   *
11588   *     console.log(interpolatedValues);
11589   *     // {opacity: 0.5, width: "150px", color: "rgb(127,127,127)"}
11590   *
11591   * @static
11592   * @method interpolate
11593   * @param {Object} from The starting values to tween from.
11594   * @param {Object} targetState The ending values to tween to.
11595   * @param {number} position The normalized position value (between `0.0` and
11596   * `1.0`) to interpolate the values between `from` and `to` for.  `from`
11597   * represents `0` and `to` represents `1`.
11598   * @param {Object.<string|Function>|string|Function} easing The easing
11599   * curve(s) to calculate the midpoint against.  You can reference any easing
11600   * function attached to `NGTweenable.prototype.formula`, or provide the easing
11601   * function(s) directly.  If omitted, this defaults to "linear".
11602   * @param {number=} opt_delay Optional delay to pad the beginning of the
11603   * interpolated tween with.  This increases the range of `position` from (`0`
11604   * through `1`) to (`0` through `1 + opt_delay`).  So, a delay of `0.5` would
11605   * increase all valid values of `position` to numbers between `0` and `1.5`.
11606   * @return {Object}
11607   */
11608  NGTweenable.interpolate = function (
11609    from, targetState, position, easing, opt_delay) {
11610
11611    var current = NGTweenable.shallowCopy({}, from);
11612    var delay = opt_delay || 0;
11613    var easingObject = NGTweenable.composeEasingObject(
11614      from, easing || 'linear');
11615
11616    mockNGTweenable.set({});
11617
11618    // Alias and reuse the _filterArgs array instead of recreating it.
11619    var filterArgs = mockNGTweenable._filterArgs;
11620    filterArgs.length = 0;
11621    filterArgs[0] = current;
11622    filterArgs[1] = from;
11623    filterArgs[2] = targetState;
11624    filterArgs[3] = easingObject;
11625
11626    // Any defined value transformation must be applied
11627    NGTweenable.applyFilter(mockNGTweenable, 'tweenCreated');
11628    NGTweenable.applyFilter(mockNGTweenable, 'beforeTween');
11629
11630    var interpolatedValues = getInterpolatedValues(
11631      from, current, targetState, position, easingObject, delay);
11632
11633    // Transform values back into their original format
11634    NGTweenable.applyFilter(mockNGTweenable, 'afterTween');
11635
11636    return interpolatedValues;
11637  };
11638
11639}());
11640
11641/**
11642 * This module adds string interpolation support to Shifty.
11643 *
11644 * The Token extension allows Shifty to tween numbers inside of strings.  Among
11645 * other things, this allows you to animate CSS properties.  For example, you
11646 * can do this:
11647 *
11648 *     var tweenable = new NGTweenable();
11649 *     tweenable.tween({
11650 *       from: { transform: 'translateX(45px)' },
11651 *       to: { transform: 'translateX(90xp)' }
11652 *     });
11653 *
11654 * `translateX(45)` will be tweened to `translateX(90)`.  To demonstrate:
11655 *
11656 *     var tweenable = new NGTweenable();
11657 *     tweenable.tween({
11658 *       from: { transform: 'translateX(45px)' },
11659 *       to: { transform: 'translateX(90px)' },
11660 *       step: function (state) {
11661 *         console.log(state.transform);
11662 *       }
11663 *     });
11664 *
11665 * The above snippet will log something like this in the console:
11666 *
11667 *     translateX(60.3px)
11668 *     ...
11669 *     translateX(76.05px)
11670 *     ...
11671 *     translateX(90px)
11672 *
11673 * Another use for this is animating colors:
11674 *
11675 *     var tweenable = new NGTweenable();
11676 *     tweenable.tween({
11677 *       from: { color: 'rgb(0,255,0)' },
11678 *       to: { color: 'rgb(255,0,255)' },
11679 *       step: function (state) {
11680 *         console.log(state.color);
11681 *       }
11682 *     });
11683 *
11684 * The above snippet will log something like this:
11685 *
11686 *     rgb(84,170,84)
11687 *     ...
11688 *     rgb(170,84,170)
11689 *     ...
11690 *     rgb(255,0,255)
11691 *
11692 * This extension also supports hexadecimal colors, in both long (`#ff00ff`)
11693 * and short (`#f0f`) forms.  Be aware that hexadecimal input values will be
11694 * converted into the equivalent RGB output values.  This is done to optimize
11695 * for performance.
11696 *
11697 *     var tweenable = new NGTweenable();
11698 *     tweenable.tween({
11699 *       from: { color: '#0f0' },
11700 *       to: { color: '#f0f' },
11701 *       step: function (state) {
11702 *         console.log(state.color);
11703 *       }
11704 *     });
11705 *
11706 * This snippet will generate the same output as the one before it because
11707 * equivalent values were supplied (just in hexadecimal form rather than RGB):
11708 *
11709 *     rgb(84,170,84)
11710 *     ...
11711 *     rgb(170,84,170)
11712 *     ...
11713 *     rgb(255,0,255)
11714 *
11715 * ## Easing support
11716 *
11717 * Easing works somewhat differently in the Token extension.  This is because
11718 * some CSS properties have multiple values in them, and you might need to
11719 * tween each value along its own easing curve.  A basic example:
11720 *
11721 *     var tweenable = new NGTweenable();
11722 *     tweenable.tween({
11723 *       from: { transform: 'translateX(0px) translateY(0px)' },
11724 *       to: { transform:   'translateX(100px) translateY(100px)' },
11725 *       easing: { transform: 'easeInQuad' },
11726 *       step: function (state) {
11727 *         console.log(state.transform);
11728 *       }
11729 *     });
11730 *
11731 * The above snippet will create values like this:
11732 *
11733 *     translateX(11.56px) translateY(11.56px)
11734 *     ...
11735 *     translateX(46.24px) translateY(46.24px)
11736 *     ...
11737 *     translateX(100px) translateY(100px)
11738 *
11739 * In this case, the values for `translateX` and `translateY` are always the
11740 * same for each step of the tween, because they have the same start and end
11741 * points and both use the same easing curve.  We can also tween `translateX`
11742 * and `translateY` along independent curves:
11743 *
11744 *     var tweenable = new NGTweenable();
11745 *     tweenable.tween({
11746 *       from: { transform: 'translateX(0px) translateY(0px)' },
11747 *       to: { transform:   'translateX(100px) translateY(100px)' },
11748 *       easing: { transform: 'easeInQuad bounce' },
11749 *       step: function (state) {
11750 *         console.log(state.transform);
11751 *       }
11752 *     });
11753 *
11754 * The above snippet will create values like this:
11755 *
11756 *     translateX(10.89px) translateY(82.35px)
11757 *     ...
11758 *     translateX(44.89px) translateY(86.73px)
11759 *     ...
11760 *     translateX(100px) translateY(100px)
11761 *
11762 * `translateX` and `translateY` are not in sync anymore, because `easeInQuad`
11763 * was specified for `translateX` and `bounce` for `translateY`.  Mixing and
11764 * matching easing curves can make for some interesting motion in your
11765 * animations.
11766 *
11767 * The order of the space-separated easing curves correspond the token values
11768 * they apply to.  If there are more token values than easing curves listed,
11769 * the last easing curve listed is used.
11770 * @submodule NGTweenable.token
11771 */
11772
11773// token function is defined above only so that dox-foundation sees it as
11774// documentation and renders it.  It is never used, and is optimized away at
11775// build time.
11776
11777;(function (NGTweenable) {
11778
11779  /**
11780   * @typedef {{
11781   *   formatString: string
11782   *   chunkNames: Array.<string>
11783   * }}
11784   * @private
11785   */
11786  var formatManifest;
11787
11788  // CONSTANTS
11789
11790  var R_NUMBER_COMPONENT = /(\d|\-|\.)/;
11791  var R_FORMAT_CHUNKS = /([^\-0-9\.]+)/g;
11792  var R_UNFORMATTED_VALUES = /[0-9.\-]+/g;
11793  var R_RGB = new RegExp(
11794    'rgb\\(' + R_UNFORMATTED_VALUES.source +
11795    (/,\s*/.source) + R_UNFORMATTED_VALUES.source +
11796    (/,\s*/.source) + R_UNFORMATTED_VALUES.source + '\\)', 'g');
11797  var R_RGB_PREFIX = /^.*\(/;
11798  var R_HEX = /#([0-9]|[a-f]){3,6}/gi;
11799  var VALUE_PLACEHOLDER = 'VAL';
11800
11801  // HELPERS
11802
11803  /**
11804   * @param {Array.number} rawValues
11805   * @param {string} prefix
11806   *
11807   * @return {Array.<string>}
11808   * @private
11809   */
11810  function getFormatChunksFrom (rawValues, prefix) {
11811    var accumulator = [];
11812
11813    var rawValuesLength = rawValues.length;
11814    var i;
11815
11816    for (i = 0; i < rawValuesLength; i++) {
11817      accumulator.push('_' + prefix + '_' + i);
11818    }
11819
11820    return accumulator;
11821  }
11822
11823  /**
11824   * @param {string} formattedString
11825   *
11826   * @return {string}
11827   * @private
11828   */
11829  function getFormatStringFrom (formattedString) {
11830    var chunks = formattedString.match(R_FORMAT_CHUNKS);
11831
11832    if (!chunks) {
11833      // chunks will be null if there were no tokens to parse in
11834      // formattedString (for example, if formattedString is '2').  Coerce
11835      // chunks to be useful here.
11836      chunks = ['', ''];
11837
11838      // If there is only one chunk, assume that the string is a number
11839      // followed by a token...
11840      // NOTE: This may be an unwise assumption.
11841    } else if (chunks.length === 1 ||
11842      // ...or if the string starts with a number component (".", "-", or a
11843      // digit)...
11844    formattedString.charAt(0).match(R_NUMBER_COMPONENT)) {
11845      // ...prepend an empty string here to make sure that the formatted number
11846      // is properly replaced by VALUE_PLACEHOLDER
11847      chunks.unshift('');
11848    }
11849
11850    return chunks.join(VALUE_PLACEHOLDER);
11851  }
11852
11853  /**
11854   * Convert all hex color values within a string to an rgb string.
11855   *
11856   * @param {Object} stateObject
11857   *
11858   * @return {Object} The modified obj
11859   * @private
11860   */
11861  function sanitizeObjectForHexProps (stateObject) {
11862    NGTweenable.each(stateObject, function (prop) {
11863      var currentProp = stateObject[prop];
11864
11865      if (typeof currentProp === 'string' && currentProp.match(R_HEX)) {
11866        stateObject[prop] = sanitizeHexChunksToRGB(currentProp);
11867      }
11868    });
11869  }
11870
11871  /**
11872   * @param {string} str
11873   *
11874   * @return {string}
11875   * @private
11876   */
11877  function  sanitizeHexChunksToRGB (str) {
11878    return filterStringChunks(R_HEX, str, convertHexToRGB);
11879  }
11880
11881  /**
11882   * @param {string} hexString
11883   *
11884   * @return {string}
11885   * @private
11886   */
11887  function convertHexToRGB (hexString) {
11888    var rgbArr = hexToRGBArray(hexString);
11889    return 'rgb(' + rgbArr[0] + ',' + rgbArr[1] + ',' + rgbArr[2] + ')';
11890  }
11891
11892  var hexToRGBArray_returnArray = [];
11893  /**
11894   * Convert a hexadecimal string to an array with three items, one each for
11895   * the red, blue, and green decimal values.
11896   *
11897   * @param {string} hex A hexadecimal string.
11898   *
11899   * @returns {Array.<number>} The converted Array of RGB values if `hex` is a
11900   * valid string, or an Array of three 0's.
11901   * @private
11902   */
11903  function hexToRGBArray (hex) {
11904
11905    hex = hex.replace(/#/, '');
11906
11907    // If the string is a shorthand three digit hex notation, normalize it to
11908    // the standard six digit notation
11909    if (hex.length === 3) {
11910      hex = hex.split('');
11911      hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
11912    }
11913
11914    hexToRGBArray_returnArray[0] = hexToDec(hex.substr(0, 2));
11915    hexToRGBArray_returnArray[1] = hexToDec(hex.substr(2, 2));
11916    hexToRGBArray_returnArray[2] = hexToDec(hex.substr(4, 2));
11917
11918    return hexToRGBArray_returnArray;
11919  }
11920
11921  /**
11922   * Convert a base-16 number to base-10.
11923   *
11924   * @param {Number|String} hex The value to convert
11925   *
11926   * @returns {Number} The base-10 equivalent of `hex`.
11927   * @private
11928   */
11929  function hexToDec (hex) {
11930    return parseInt(hex, 16);
11931  }
11932
11933  /**
11934   * Runs a filter operation on all chunks of a string that match a RegExp
11935   *
11936   * @param {RegExp} pattern
11937   * @param {string} unfilteredString
11938   * @param {function(string)} filter
11939   *
11940   * @return {string}
11941   * @private
11942   */
11943  function filterStringChunks (pattern, unfilteredString, filter) {
11944    var pattenMatches = unfilteredString.match(pattern);
11945    var filteredString = unfilteredString.replace(pattern, VALUE_PLACEHOLDER);
11946
11947    if (pattenMatches) {
11948      var pattenMatchesLength = pattenMatches.length;
11949      var currentChunk;
11950
11951      for (var i = 0; i < pattenMatchesLength; i++) {
11952        currentChunk = pattenMatches.shift();
11953        filteredString = filteredString.replace(
11954          VALUE_PLACEHOLDER, filter(currentChunk));
11955      }
11956    }
11957
11958    return filteredString;
11959  }
11960
11961  /**
11962   * Check for floating point values within rgb strings and rounds them.
11963   *
11964   * @param {string} formattedString
11965   *
11966   * @return {string}
11967   * @private
11968   */
11969  function sanitizeRGBChunks (formattedString) {
11970    return filterStringChunks(R_RGB, formattedString, sanitizeRGBChunk);
11971  }
11972
11973  /**
11974   * @param {string} rgbChunk
11975   *
11976   * @return {string}
11977   * @private
11978   */
11979  function sanitizeRGBChunk (rgbChunk) {
11980    var numbers = rgbChunk.match(R_UNFORMATTED_VALUES);
11981    var numbersLength = numbers.length;
11982    var sanitizedString = rgbChunk.match(R_RGB_PREFIX)[0];
11983
11984    for (var i = 0; i < numbersLength; i++) {
11985      sanitizedString += parseInt(numbers[i], 10) + ',';
11986    }
11987
11988    sanitizedString = sanitizedString.slice(0, -1) + ')';
11989
11990    return sanitizedString;
11991  }
11992
11993  /**
11994   * @param {Object} stateObject
11995   *
11996   * @return {Object} An Object of formatManifests that correspond to
11997   * the string properties of stateObject
11998   * @private
11999   */
12000  function getFormatManifests (stateObject) {
12001    var manifestAccumulator = {};
12002
12003    NGTweenable.each(stateObject, function (prop) {
12004      var currentProp = stateObject[prop];
12005
12006      if (typeof currentProp === 'string') {
12007        var rawValues = getValuesFrom(currentProp);
12008
12009        manifestAccumulator[prop] = {
12010          'formatString': getFormatStringFrom(currentProp)
12011          ,'chunkNames': getFormatChunksFrom(rawValues, prop)
12012        };
12013      }
12014    });
12015
12016    return manifestAccumulator;
12017  }
12018
12019  /**
12020   * @param {Object} stateObject
12021   * @param {Object} formatManifests
12022   * @private
12023   */
12024  function expandFormattedProperties (stateObject, formatManifests) {
12025    NGTweenable.each(formatManifests, function (prop) {
12026      var currentProp = stateObject[prop];
12027      var rawValues = getValuesFrom(currentProp);
12028      var rawValuesLength = rawValues.length;
12029
12030      for (var i = 0; i < rawValuesLength; i++) {
12031        stateObject[formatManifests[prop].chunkNames[i]] = +rawValues[i];
12032      }
12033
12034      delete stateObject[prop];
12035    });
12036  }
12037
12038  /**
12039   * @param {Object} stateObject
12040   * @param {Object} formatManifests
12041   * @private
12042   */
12043  function collapseFormattedProperties (stateObject, formatManifests) {
12044    NGTweenable.each(formatManifests, function (prop) {
12045      var currentProp = stateObject[prop];
12046      var formatChunks = extractPropertyChunks(
12047        stateObject, formatManifests[prop].chunkNames);
12048      var valuesList = getValuesList(
12049        formatChunks, formatManifests[prop].chunkNames);
12050      currentProp = getFormattedValues(
12051        formatManifests[prop].formatString, valuesList);
12052      stateObject[prop] = sanitizeRGBChunks(currentProp);
12053    });
12054  }
12055
12056  /**
12057   * @param {Object} stateObject
12058   * @param {Array.<string>} chunkNames
12059   *
12060   * @return {Object} The extracted value chunks.
12061   * @private
12062   */
12063  function extractPropertyChunks (stateObject, chunkNames) {
12064    var extractedValues = {};
12065    var currentChunkName, chunkNamesLength = chunkNames.length;
12066
12067    for (var i = 0; i < chunkNamesLength; i++) {
12068      currentChunkName = chunkNames[i];
12069      extractedValues[currentChunkName] = stateObject[currentChunkName];
12070      delete stateObject[currentChunkName];
12071    }
12072
12073    return extractedValues;
12074  }
12075
12076  var getValuesList_accumulator = [];
12077  /**
12078   * @param {Object} stateObject
12079   * @param {Array.<string>} chunkNames
12080   *
12081   * @return {Array.<number>}
12082   * @private
12083   */
12084  function getValuesList (stateObject, chunkNames) {
12085    getValuesList_accumulator.length = 0;
12086    var chunkNamesLength = chunkNames.length;
12087
12088    for (var i = 0; i < chunkNamesLength; i++) {
12089      getValuesList_accumulator.push(stateObject[chunkNames[i]]);
12090    }
12091
12092    return getValuesList_accumulator;
12093  }
12094
12095  /**
12096   * @param {string} formatString
12097   * @param {Array.<number>} rawValues
12098   *
12099   * @return {string}
12100   * @private
12101   */
12102  function getFormattedValues (formatString, rawValues) {
12103    var formattedValueString = formatString;
12104    var rawValuesLength = rawValues.length;
12105
12106    for (var i = 0; i < rawValuesLength; i++) {
12107      formattedValueString = formattedValueString.replace(
12108        VALUE_PLACEHOLDER, +rawValues[i].toFixed(4));
12109    }
12110
12111    return formattedValueString;
12112  }
12113
12114  /**
12115   * Note: It's the duty of the caller to convert the Array elements of the
12116   * return value into numbers.  This is a performance optimization.
12117   *
12118   * @param {string} formattedString
12119   *
12120   * @return {Array.<string>|null}
12121   * @private
12122   */
12123  function getValuesFrom (formattedString) {
12124    return formattedString.match(R_UNFORMATTED_VALUES);
12125  }
12126
12127  /**
12128   * @param {Object} easingObject
12129   * @param {Object} tokenData
12130   * @private
12131   */
12132  function expandEasingObject (easingObject, tokenData) {
12133    NGTweenable.each(tokenData, function (prop) {
12134      var currentProp = tokenData[prop];
12135      var chunkNames = currentProp.chunkNames;
12136      var chunkLength = chunkNames.length;
12137
12138      var easing = easingObject[prop];
12139      var i;
12140
12141      if (typeof easing === 'string') {
12142        var easingChunks = easing.split(' ');
12143        var lastEasingChunk = easingChunks[easingChunks.length - 1];
12144
12145        for (i = 0; i < chunkLength; i++) {
12146          easingObject[chunkNames[i]] = easingChunks[i] || lastEasingChunk;
12147        }
12148
12149      } else {
12150        for (i = 0; i < chunkLength; i++) {
12151          easingObject[chunkNames[i]] = easing;
12152        }
12153      }
12154
12155      delete easingObject[prop];
12156    });
12157  }
12158
12159  /**
12160   * @param {Object} easingObject
12161   * @param {Object} tokenData
12162   * @private
12163   */
12164  function collapseEasingObject (easingObject, tokenData) {
12165    NGTweenable.each(tokenData, function (prop) {
12166      var currentProp = tokenData[prop];
12167      var chunkNames = currentProp.chunkNames;
12168      var chunkLength = chunkNames.length;
12169
12170      var firstEasing = easingObject[chunkNames[0]];
12171      var typeofEasings = typeof firstEasing;
12172
12173      if (typeofEasings === 'string') {
12174        var composedEasingString = '';
12175
12176        for (var i = 0; i < chunkLength; i++) {
12177          composedEasingString += ' ' + easingObject[chunkNames[i]];
12178          delete easingObject[chunkNames[i]];
12179        }
12180
12181        easingObject[prop] = composedEasingString.substr(1);
12182      } else {
12183        easingObject[prop] = firstEasing;
12184      }
12185    });
12186  }
12187
12188  NGTweenable.prototype.filter.token = {
12189    'tweenCreated': function (currentState, fromState, toState, easingObject) {
12190      sanitizeObjectForHexProps(currentState);
12191      sanitizeObjectForHexProps(fromState);
12192      sanitizeObjectForHexProps(toState);
12193      this._tokenData = getFormatManifests(currentState);
12194    },
12195
12196    'beforeTween': function (currentState, fromState, toState, easingObject) {
12197      expandEasingObject(easingObject, this._tokenData);
12198      expandFormattedProperties(currentState, this._tokenData);
12199      expandFormattedProperties(fromState, this._tokenData);
12200      expandFormattedProperties(toState, this._tokenData);
12201    },
12202
12203    'afterTween': function (currentState, fromState, toState, easingObject) {
12204      collapseFormattedProperties(currentState, this._tokenData);
12205      collapseFormattedProperties(fromState, this._tokenData);
12206      collapseFormattedProperties(toState, this._tokenData);
12207      collapseEasingObject(easingObject, this._tokenData);
12208    }
12209  };
12210
12211} (NGTweenable));
12212
12213}).call(null);
12214
12215
12216
12217
12218//##########################################################################################################################
12219//## HAMMER.JS #############################################################################################################
12220//##########################################################################################################################
12221
12222// HAMMER.JS
12223
12224// external module EMBEDED in nanogallery
12225// NGY BUILD:
12226// replace "Hammer" with "NGHammer" (case sensitive)
12227// replace "var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined;" with "var SUPPORT_POINTER_EVENTS = false;"
12228// replace "define.amd" with "define.amdDISABLED"
12229
12230
12231
12232/*! NGHammer.JS - v2.0.7 - 2016-04-22
12233 * http://hammerjs.github.io/
12234 *
12235 * Copyright (c) 2016 Jorik Tangelder;
12236 * Licensed under the MIT license */
12237(function(window, document, exportName, undefined) {
12238  'use strict';
12239
12240var VENDOR_PREFIXES = ['', 'webkit', 'Moz', 'MS', 'ms', 'o'];
12241var TEST_ELEMENT = document.createElement('div');
12242
12243var TYPE_FUNCTION = 'function';
12244
12245var round = Math.round;
12246var abs = Math.abs;
12247var now = Date.now;
12248
12249/**
12250 * set a timeout with a given scope
12251 * @param {Function} fn
12252 * @param {Number} timeout
12253 * @param {Object} context
12254 * @returns {number}
12255 */
12256function setTimeoutContext(fn, timeout, context) {
12257    return setTimeout(bindFn(fn, context), timeout);
12258}
12259
12260/**
12261 * if the argument is an array, we want to execute the fn on each entry
12262 * if it aint an array we don't want to do a thing.
12263 * this is used by all the methods that accept a single and array argument.
12264 * @param {*|Array} arg
12265 * @param {String} fn
12266 * @param {Object} [context]
12267 * @returns {Boolean}
12268 */
12269function invokeArrayArg(arg, fn, context) {
12270    if (Array.isArray(arg)) {
12271        each(arg, context[fn], context);
12272        return true;
12273    }
12274    return false;
12275}
12276
12277/**
12278 * walk objects and arrays
12279 * @param {Object} obj
12280 * @param {Function} iterator
12281 * @param {Object} context
12282 */
12283function each(obj, iterator, context) {
12284    var i;
12285
12286    if (!obj) {
12287        return;
12288    }
12289
12290    if (obj.forEach) {
12291        obj.forEach(iterator, context);
12292    } else if (obj.length !== undefined) {
12293        i = 0;
12294        while (i < obj.length) {
12295            iterator.call(context, obj[i], i, obj);
12296            i++;
12297        }
12298    } else {
12299        for (i in obj) {
12300            obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj);
12301        }
12302    }
12303}
12304
12305/**
12306 * wrap a method with a deprecation warning and stack trace
12307 * @param {Function} method
12308 * @param {String} name
12309 * @param {String} message
12310 * @returns {Function} A new function wrapping the supplied method.
12311 */
12312function deprecate(method, name, message) {
12313    var deprecationMessage = 'DEPRECATED METHOD: ' + name + '\n' + message + ' AT \n';
12314    return function() {
12315        var e = new Error('get-stack-trace');
12316        var stack = e && e.stack ? e.stack.replace(/^[^\(]+?[\n$]/gm, '')
12317            .replace(/^\s+at\s+/gm, '')
12318            .replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@') : 'Unknown Stack Trace';
12319
12320        var log = window.console && (window.console.warn || window.console.log);
12321        if (log) {
12322            log.call(window.console, deprecationMessage, stack);
12323        }
12324        return method.apply(this, arguments);
12325    };
12326}
12327
12328/**
12329 * extend object.
12330 * means that properties in dest will be overwritten by the ones in src.
12331 * @param {Object} target
12332 * @param {...Object} objects_to_assign
12333 * @returns {Object} target
12334 */
12335var assign;
12336if (typeof Object.assign !== 'function') {
12337    assign = function assign(target) {
12338        if (target === undefined || target === null) {
12339            throw new TypeError('Cannot convert undefined or null to object');
12340        }
12341
12342        var output = Object(target);
12343        for (var index = 1; index < arguments.length; index++) {
12344            var source = arguments[index];
12345            if (source !== undefined && source !== null) {
12346                for (var nextKey in source) {
12347                    if (source.hasOwnProperty(nextKey)) {
12348                        output[nextKey] = source[nextKey];
12349                    }
12350                }
12351            }
12352        }
12353        return output;
12354    };
12355} else {
12356    assign = Object.assign;
12357}
12358
12359/**
12360 * extend object.
12361 * means that properties in dest will be overwritten by the ones in src.
12362 * @param {Object} dest
12363 * @param {Object} src
12364 * @param {Boolean} [merge=false]
12365 * @returns {Object} dest
12366 */
12367var extend = deprecate(function extend(dest, src, merge) {
12368    var keys = Object.keys(src);
12369    var i = 0;
12370    while (i < keys.length) {
12371        if (!merge || (merge && dest[keys[i]] === undefined)) {
12372            dest[keys[i]] = src[keys[i]];
12373        }
12374        i++;
12375    }
12376    return dest;
12377}, 'extend', 'Use `assign`.');
12378
12379/**
12380 * merge the values from src in the dest.
12381 * means that properties that exist in dest will not be overwritten by src
12382 * @param {Object} dest
12383 * @param {Object} src
12384 * @returns {Object} dest
12385 */
12386var merge = deprecate(function merge(dest, src) {
12387    return extend(dest, src, true);
12388}, 'merge', 'Use `assign`.');
12389
12390/**
12391 * simple class inheritance
12392 * @param {Function} child
12393 * @param {Function} base
12394 * @param {Object} [properties]
12395 */
12396function inherit(child, base, properties) {
12397    var baseP = base.prototype,
12398        childP;
12399
12400    childP = child.prototype = Object.create(baseP);
12401    childP.constructor = child;
12402    childP._super = baseP;
12403
12404    if (properties) {
12405        assign(childP, properties);
12406    }
12407}
12408
12409/**
12410 * simple function bind
12411 * @param {Function} fn
12412 * @param {Object} context
12413 * @returns {Function}
12414 */
12415function bindFn(fn, context) {
12416    return function boundFn() {
12417        return fn.apply(context, arguments);
12418    };
12419}
12420
12421/**
12422 * let a boolean value also be a function that must return a boolean
12423 * this first item in args will be used as the context
12424 * @param {Boolean|Function} val
12425 * @param {Array} [args]
12426 * @returns {Boolean}
12427 */
12428function boolOrFn(val, args) {
12429    if (typeof val == TYPE_FUNCTION) {
12430        return val.apply(args ? args[0] || undefined : undefined, args);
12431    }
12432    return val;
12433}
12434
12435/**
12436 * use the val2 when val1 is undefined
12437 * @param {*} val1
12438 * @param {*} val2
12439 * @returns {*}
12440 */
12441function ifUndefined(val1, val2) {
12442    return (val1 === undefined) ? val2 : val1;
12443}
12444
12445/**
12446 * addEventListener with multiple events at once
12447 * @param {EventTarget} target
12448 * @param {String} types
12449 * @param {Function} handler
12450 */
12451function addEventListeners(target, types, handler) {
12452    each(splitStr(types), function(type) {
12453        target.addEventListener(type, handler, false);
12454    });
12455}
12456
12457/**
12458 * removeEventListener with multiple events at once
12459 * @param {EventTarget} target
12460 * @param {String} types
12461 * @param {Function} handler
12462 */
12463function removeEventListeners(target, types, handler) {
12464    each(splitStr(types), function(type) {
12465        target.removeEventListener(type, handler, false);
12466    });
12467}
12468
12469/**
12470 * find if a node is in the given parent
12471 * @method hasParent
12472 * @param {HTMLElement} node
12473 * @param {HTMLElement} parent
12474 * @return {Boolean} found
12475 */
12476function hasParent(node, parent) {
12477    while (node) {
12478        if (node == parent) {
12479            return true;
12480        }
12481        node = node.parentNode;
12482    }
12483    return false;
12484}
12485
12486/**
12487 * small indexOf wrapper
12488 * @param {String} str
12489 * @param {String} find
12490 * @returns {Boolean} found
12491 */
12492function inStr(str, find) {
12493    return str.indexOf(find) > -1;
12494}
12495
12496/**
12497 * split string on whitespace
12498 * @param {String} str
12499 * @returns {Array} words
12500 */
12501function splitStr(str) {
12502    return str.trim().split(/\s+/g);
12503}
12504
12505/**
12506 * find if a array contains the object using indexOf or a simple polyFill
12507 * @param {Array} src
12508 * @param {String} find
12509 * @param {String} [findByKey]
12510 * @return {Boolean|Number} false when not found, or the index
12511 */
12512function inArray(src, find, findByKey) {
12513    if (src.indexOf && !findByKey) {
12514        return src.indexOf(find);
12515    } else {
12516        var i = 0;
12517        while (i < src.length) {
12518            if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) {
12519                return i;
12520            }
12521            i++;
12522        }
12523        return -1;
12524    }
12525}
12526
12527/**
12528 * convert array-like objects to real arrays
12529 * @param {Object} obj
12530 * @returns {Array}
12531 */
12532function toArray(obj) {
12533    return Array.prototype.slice.call(obj, 0);
12534}
12535
12536/**
12537 * unique array with objects based on a key (like 'id') or just by the array's value
12538 * @param {Array} src [{id:1},{id:2},{id:1}]
12539 * @param {String} [key]
12540 * @param {Boolean} [sort=False]
12541 * @returns {Array} [{id:1},{id:2}]
12542 */
12543function uniqueArray(src, key, sort) {
12544    var results = [];
12545    var values = [];
12546    var i = 0;
12547
12548    while (i < src.length) {
12549        var val = key ? src[i][key] : src[i];
12550        if (inArray(values, val) < 0) {
12551            results.push(src[i]);
12552        }
12553        values[i] = val;
12554        i++;
12555    }
12556
12557    if (sort) {
12558        if (!key) {
12559            results = results.sort();
12560        } else {
12561            results = results.sort(function sortUniqueArray(a, b) {
12562                return a[key] > b[key];
12563            });
12564        }
12565    }
12566
12567    return results;
12568}
12569
12570/**
12571 * get the prefixed property
12572 * @param {Object} obj
12573 * @param {String} property
12574 * @returns {String|Undefined} prefixed
12575 */
12576function prefixed(obj, property) {
12577    var prefix, prop;
12578    var camelProp = property[0].toUpperCase() + property.slice(1);
12579
12580    var i = 0;
12581    while (i < VENDOR_PREFIXES.length) {
12582        prefix = VENDOR_PREFIXES[i];
12583        prop = (prefix) ? prefix + camelProp : property;
12584
12585        if (prop in obj) {
12586            return prop;
12587        }
12588        i++;
12589    }
12590    return undefined;
12591}
12592
12593/**
12594 * get a unique id
12595 * @returns {number} uniqueId
12596 */
12597var _uniqueId = 1;
12598function uniqueId() {
12599    return _uniqueId++;
12600}
12601
12602/**
12603 * get the window object of an element
12604 * @param {HTMLElement} element
12605 * @returns {DocumentView|Window}
12606 */
12607function getWindowForElement(element) {
12608    var doc = element.ownerDocument || element;
12609    return (doc.defaultView || doc.parentWindow || window);
12610}
12611
12612var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
12613
12614var SUPPORT_TOUCH = ('ontouchstart' in window);
12615// var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined;
12616var SUPPORT_POINTER_EVENTS = false;
12617var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent);
12618
12619var INPUT_TYPE_TOUCH = 'touch';
12620var INPUT_TYPE_PEN = 'pen';
12621var INPUT_TYPE_MOUSE = 'mouse';
12622var INPUT_TYPE_KINECT = 'kinect';
12623
12624var COMPUTE_INTERVAL = 25;
12625
12626var INPUT_START = 1;
12627var INPUT_MOVE = 2;
12628var INPUT_END = 4;
12629var INPUT_CANCEL = 8;
12630
12631var DIRECTION_NONE = 1;
12632var DIRECTION_LEFT = 2;
12633var DIRECTION_RIGHT = 4;
12634var DIRECTION_UP = 8;
12635var DIRECTION_DOWN = 16;
12636
12637var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT;
12638var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN;
12639var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;
12640
12641var PROPS_XY = ['x', 'y'];
12642var PROPS_CLIENT_XY = ['clientX', 'clientY'];
12643
12644/**
12645 * create new input type manager
12646 * @param {Manager} manager
12647 * @param {Function} callback
12648 * @returns {Input}
12649 * @constructor
12650 */
12651function Input(manager, callback) {
12652    var self = this;
12653    this.manager = manager;
12654    this.callback = callback;
12655    this.element = manager.element;
12656    this.target = manager.options.inputTarget;
12657
12658    // smaller wrapper around the handler, for the scope and the enabled state of the manager,
12659    // so when disabled the input events are completely bypassed.
12660    this.domHandler = function(ev) {
12661        if (boolOrFn(manager.options.enable, [manager])) {
12662            self.handler(ev);
12663        }
12664    };
12665
12666    this.init();
12667
12668}
12669
12670Input.prototype = {
12671    /**
12672     * should handle the inputEvent data and trigger the callback
12673     * @virtual
12674     */
12675    handler: function() { },
12676
12677    /**
12678     * bind the events
12679     */
12680    init: function() {
12681        this.evEl && addEventListeners(this.element, this.evEl, this.domHandler);
12682        this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler);
12683        this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);
12684    },
12685
12686    /**
12687     * unbind the events
12688     */
12689    destroy: function() {
12690        this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler);
12691        this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler);
12692        this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);
12693    }
12694};
12695
12696/**
12697 * create new input type manager
12698 * called by the Manager constructor
12699 * @param {NGHammer} manager
12700 * @returns {Input}
12701 */
12702function createInputInstance(manager) {
12703    var Type;
12704    var inputClass = manager.options.inputClass;
12705
12706    if (inputClass) {
12707        Type = inputClass;
12708    } else if (SUPPORT_POINTER_EVENTS) {
12709        Type = PointerEventInput;
12710    } else if (SUPPORT_ONLY_TOUCH) {
12711        Type = TouchInput;
12712    } else if (!SUPPORT_TOUCH) {
12713        Type = MouseInput;
12714    } else {
12715        Type = TouchMouseInput;
12716    }
12717    return new (Type)(manager, inputHandler);
12718}
12719
12720/**
12721 * handle input events
12722 * @param {Manager} manager
12723 * @param {String} eventType
12724 * @param {Object} input
12725 */
12726function inputHandler(manager, eventType, input) {
12727    var pointersLen = input.pointers.length;
12728    var changedPointersLen = input.changedPointers.length;
12729    var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0));
12730    var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0));
12731
12732    input.isFirst = !!isFirst;
12733    input.isFinal = !!isFinal;
12734
12735    if (isFirst) {
12736        manager.session = {};
12737    }
12738
12739    // source event is the normalized value of the domEvents
12740    // like 'touchstart, mouseup, pointerdown'
12741    input.eventType = eventType;
12742
12743    // compute scale, rotation etc
12744    computeInputData(manager, input);
12745
12746    // emit secret event
12747    manager.emit('hammer.input', input);
12748
12749    manager.recognize(input);
12750    manager.session.prevInput = input;
12751}
12752
12753/**
12754 * extend the data with some usable properties like scale, rotate, velocity etc
12755 * @param {Object} manager
12756 * @param {Object} input
12757 */
12758function computeInputData(manager, input) {
12759    var session = manager.session;
12760    var pointers = input.pointers;
12761    var pointersLength = pointers.length;
12762
12763    // store the first input to calculate the distance and direction
12764    if (!session.firstInput) {
12765        session.firstInput = simpleCloneInputData(input);
12766    }
12767
12768    // to compute scale and rotation we need to store the multiple touches
12769    if (pointersLength > 1 && !session.firstMultiple) {
12770        session.firstMultiple = simpleCloneInputData(input);
12771    } else if (pointersLength === 1) {
12772        session.firstMultiple = false;
12773    }
12774
12775    var firstInput = session.firstInput;
12776    var firstMultiple = session.firstMultiple;
12777    var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center;
12778
12779    var center = input.center = getCenter(pointers);
12780    input.timeStamp = now();
12781    input.deltaTime = input.timeStamp - firstInput.timeStamp;
12782
12783    input.angle = getAngle(offsetCenter, center);
12784    input.distance = getDistance(offsetCenter, center);
12785
12786    computeDeltaXY(session, input);
12787    input.offsetDirection = getDirection(input.deltaX, input.deltaY);
12788
12789    var overallVelocity = getVelocity(input.deltaTime, input.deltaX, input.deltaY);
12790    input.overallVelocityX = overallVelocity.x;
12791    input.overallVelocityY = overallVelocity.y;
12792    input.overallVelocity = (abs(overallVelocity.x) > abs(overallVelocity.y)) ? overallVelocity.x : overallVelocity.y;
12793
12794    input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1;
12795    input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0;
12796
12797    input.maxPointers = !session.prevInput ? input.pointers.length : ((input.pointers.length >
12798        session.prevInput.maxPointers) ? input.pointers.length : session.prevInput.maxPointers);
12799
12800    computeIntervalInputData(session, input);
12801
12802    // find the correct target
12803    var target = manager.element;
12804    if (hasParent(input.srcEvent.target, target)) {
12805        target = input.srcEvent.target;
12806    }
12807    input.target = target;
12808}
12809
12810function computeDeltaXY(session, input) {
12811    var center = input.center;
12812    var offset = session.offsetDelta || {};
12813    var prevDelta = session.prevDelta || {};
12814    var prevInput = session.prevInput || {};
12815
12816    if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) {
12817        prevDelta = session.prevDelta = {
12818            x: prevInput.deltaX || 0,
12819            y: prevInput.deltaY || 0
12820        };
12821
12822        offset = session.offsetDelta = {
12823            x: center.x,
12824            y: center.y
12825        };
12826    }
12827
12828    input.deltaX = prevDelta.x + (center.x - offset.x);
12829    input.deltaY = prevDelta.y + (center.y - offset.y);
12830}
12831
12832/**
12833 * velocity is calculated every x ms
12834 * @param {Object} session
12835 * @param {Object} input
12836 */
12837function computeIntervalInputData(session, input) {
12838    var last = session.lastInterval || input,
12839        deltaTime = input.timeStamp - last.timeStamp,
12840        velocity, velocityX, velocityY, direction;
12841
12842    if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) {
12843        var deltaX = input.deltaX - last.deltaX;
12844        var deltaY = input.deltaY - last.deltaY;
12845
12846        var v = getVelocity(deltaTime, deltaX, deltaY);
12847        velocityX = v.x;
12848        velocityY = v.y;
12849        velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y;
12850        direction = getDirection(deltaX, deltaY);
12851
12852        session.lastInterval = input;
12853    } else {
12854        // use latest velocity info if it doesn't overtake a minimum period
12855        velocity = last.velocity;
12856        velocityX = last.velocityX;
12857        velocityY = last.velocityY;
12858        direction = last.direction;
12859    }
12860
12861    input.velocity = velocity;
12862    input.velocityX = velocityX;
12863    input.velocityY = velocityY;
12864    input.direction = direction;
12865}
12866
12867/**
12868 * create a simple clone from the input used for storage of firstInput and firstMultiple
12869 * @param {Object} input
12870 * @returns {Object} clonedInputData
12871 */
12872function simpleCloneInputData(input) {
12873    // make a simple copy of the pointers because we will get a reference if we don't
12874    // we only need clientXY for the calculations
12875    var pointers = [];
12876    var i = 0;
12877    while (i < input.pointers.length) {
12878        pointers[i] = {
12879            clientX: round(input.pointers[i].clientX),
12880            clientY: round(input.pointers[i].clientY)
12881        };
12882        i++;
12883    }
12884
12885    return {
12886        timeStamp: now(),
12887        pointers: pointers,
12888        center: getCenter(pointers),
12889        deltaX: input.deltaX,
12890        deltaY: input.deltaY
12891    };
12892}
12893
12894/**
12895 * get the center of all the pointers
12896 * @param {Array} pointers
12897 * @return {Object} center contains `x` and `y` properties
12898 */
12899function getCenter(pointers) {
12900    var pointersLength = pointers.length;
12901
12902    // no need to loop when only one touch
12903    if (pointersLength === 1) {
12904        return {
12905            x: round(pointers[0].clientX),
12906            y: round(pointers[0].clientY)
12907        };
12908    }
12909
12910    var x = 0, y = 0, i = 0;
12911    while (i < pointersLength) {
12912        x += pointers[i].clientX;
12913        y += pointers[i].clientY;
12914        i++;
12915    }
12916
12917    return {
12918        x: round(x / pointersLength),
12919        y: round(y / pointersLength)
12920    };
12921}
12922
12923/**
12924 * calculate the velocity between two points. unit is in px per ms.
12925 * @param {Number} deltaTime
12926 * @param {Number} x
12927 * @param {Number} y
12928 * @return {Object} velocity `x` and `y`
12929 */
12930function getVelocity(deltaTime, x, y) {
12931    return {
12932        x: x / deltaTime || 0,
12933        y: y / deltaTime || 0
12934    };
12935}
12936
12937/**
12938 * get the direction between two points
12939 * @param {Number} x
12940 * @param {Number} y
12941 * @return {Number} direction
12942 */
12943function getDirection(x, y) {
12944    if (x === y) {
12945        return DIRECTION_NONE;
12946    }
12947
12948    if (abs(x) >= abs(y)) {
12949        return x < 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
12950    }
12951    return y < 0 ? DIRECTION_UP : DIRECTION_DOWN;
12952}
12953
12954/**
12955 * calculate the absolute distance between two points
12956 * @param {Object} p1 {x, y}
12957 * @param {Object} p2 {x, y}
12958 * @param {Array} [props] containing x and y keys
12959 * @return {Number} distance
12960 */
12961function getDistance(p1, p2, props) {
12962    if (!props) {
12963        props = PROPS_XY;
12964    }
12965    var x = p2[props[0]] - p1[props[0]],
12966        y = p2[props[1]] - p1[props[1]];
12967
12968    return Math.sqrt((x * x) + (y * y));
12969}
12970
12971/**
12972 * calculate the angle between two coordinates
12973 * @param {Object} p1
12974 * @param {Object} p2
12975 * @param {Array} [props] containing x and y keys
12976 * @return {Number} angle
12977 */
12978function getAngle(p1, p2, props) {
12979    if (!props) {
12980        props = PROPS_XY;
12981    }
12982    var x = p2[props[0]] - p1[props[0]],
12983        y = p2[props[1]] - p1[props[1]];
12984    return Math.atan2(y, x) * 180 / Math.PI;
12985}
12986
12987/**
12988 * calculate the rotation degrees between two pointersets
12989 * @param {Array} start array of pointers
12990 * @param {Array} end array of pointers
12991 * @return {Number} rotation
12992 */
12993function getRotation(start, end) {
12994    return getAngle(end[1], end[0], PROPS_CLIENT_XY) + getAngle(start[1], start[0], PROPS_CLIENT_XY);
12995}
12996
12997/**
12998 * calculate the scale factor between two pointersets
12999 * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
13000 * @param {Array} start array of pointers
13001 * @param {Array} end array of pointers
13002 * @return {Number} scale
13003 */
13004function getScale(start, end) {
13005    return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY);
13006}
13007
13008var MOUSE_INPUT_MAP = {
13009    mousedown: INPUT_START,
13010    mousemove: INPUT_MOVE,
13011    mouseup: INPUT_END
13012};
13013
13014var MOUSE_ELEMENT_EVENTS = 'mousedown';
13015var MOUSE_WINDOW_EVENTS = 'mousemove mouseup';
13016
13017/**
13018 * Mouse events input
13019 * @constructor
13020 * @extends Input
13021 */
13022function MouseInput() {
13023    this.evEl = MOUSE_ELEMENT_EVENTS;
13024    this.evWin = MOUSE_WINDOW_EVENTS;
13025
13026    this.pressed = false; // mousedown state
13027
13028    Input.apply(this, arguments);
13029}
13030
13031inherit(MouseInput, Input, {
13032    /**
13033     * handle mouse events
13034     * @param {Object} ev
13035     */
13036    handler: function MEhandler(ev) {
13037        var eventType = MOUSE_INPUT_MAP[ev.type];
13038
13039        // on start we want to have the left mouse button down
13040        if (eventType & INPUT_START && ev.button === 0) {
13041            this.pressed = true;
13042        }
13043
13044        if (eventType & INPUT_MOVE && ev.which !== 1) {
13045            eventType = INPUT_END;
13046        }
13047
13048        // mouse must be down
13049        if (!this.pressed) {
13050            return;
13051        }
13052
13053        if (eventType & INPUT_END) {
13054            this.pressed = false;
13055        }
13056
13057        this.callback(this.manager, eventType, {
13058            pointers: [ev],
13059            changedPointers: [ev],
13060            pointerType: INPUT_TYPE_MOUSE,
13061            srcEvent: ev
13062        });
13063    }
13064});
13065
13066var POINTER_INPUT_MAP = {
13067    pointerdown: INPUT_START,
13068    pointermove: INPUT_MOVE,
13069    pointerup: INPUT_END,
13070    pointercancel: INPUT_CANCEL,
13071    pointerout: INPUT_CANCEL
13072};
13073
13074// in IE10 the pointer types is defined as an enum
13075var IE10_POINTER_TYPE_ENUM = {
13076    2: INPUT_TYPE_TOUCH,
13077    3: INPUT_TYPE_PEN,
13078    4: INPUT_TYPE_MOUSE,
13079    5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816
13080};
13081
13082var POINTER_ELEMENT_EVENTS = 'pointerdown';
13083var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel';
13084
13085// IE10 has prefixed support, and case-sensitive
13086if (window.MSPointerEvent && !window.PointerEvent) {
13087    POINTER_ELEMENT_EVENTS = 'MSPointerDown';
13088    POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel';
13089}
13090
13091/**
13092 * Pointer events input
13093 * @constructor
13094 * @extends Input
13095 */
13096function PointerEventInput() {
13097    this.evEl = POINTER_ELEMENT_EVENTS;
13098    this.evWin = POINTER_WINDOW_EVENTS;
13099
13100    Input.apply(this, arguments);
13101
13102    this.store = (this.manager.session.pointerEvents = []);
13103}
13104
13105inherit(PointerEventInput, Input, {
13106    /**
13107     * handle mouse events
13108     * @param {Object} ev
13109     */
13110    handler: function PEhandler(ev) {
13111        var store = this.store;
13112        var removePointer = false;
13113
13114        var eventTypeNormalized = ev.type.toLowerCase().replace('ms', '');
13115        var eventType = POINTER_INPUT_MAP[eventTypeNormalized];
13116        var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType;
13117
13118        var isTouch = (pointerType == INPUT_TYPE_TOUCH);
13119
13120        // get index of the event in the store
13121        var storeIndex = inArray(store, ev.pointerId, 'pointerId');
13122
13123        // start and mouse must be down
13124        if (eventType & INPUT_START && (ev.button === 0 || isTouch)) {
13125            if (storeIndex < 0) {
13126                store.push(ev);
13127                storeIndex = store.length - 1;
13128            }
13129        } else if (eventType & (INPUT_END | INPUT_CANCEL)) {
13130            removePointer = true;
13131        }
13132
13133        // it not found, so the pointer hasn't been down (so it's probably a hover)
13134        if (storeIndex < 0) {
13135            return;
13136        }
13137
13138        // update the event in the store
13139        store[storeIndex] = ev;
13140
13141        this.callback(this.manager, eventType, {
13142            pointers: store,
13143            changedPointers: [ev],
13144            pointerType: pointerType,
13145            srcEvent: ev
13146        });
13147
13148        if (removePointer) {
13149            // remove from the store
13150            store.splice(storeIndex, 1);
13151        }
13152    }
13153});
13154
13155var SINGLE_TOUCH_INPUT_MAP = {
13156    touchstart: INPUT_START,
13157    touchmove: INPUT_MOVE,
13158    touchend: INPUT_END,
13159    touchcancel: INPUT_CANCEL
13160};
13161
13162var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart';
13163var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel';
13164
13165/**
13166 * Touch events input
13167 * @constructor
13168 * @extends Input
13169 */
13170function SingleTouchInput() {
13171    this.evTarget = SINGLE_TOUCH_TARGET_EVENTS;
13172    this.evWin = SINGLE_TOUCH_WINDOW_EVENTS;
13173    this.started = false;
13174
13175    Input.apply(this, arguments);
13176}
13177
13178inherit(SingleTouchInput, Input, {
13179    handler: function TEhandler(ev) {
13180        var type = SINGLE_TOUCH_INPUT_MAP[ev.type];
13181
13182        // should we handle the touch events?
13183        if (type === INPUT_START) {
13184            this.started = true;
13185        }
13186
13187        if (!this.started) {
13188            return;
13189        }
13190
13191        var touches = normalizeSingleTouches.call(this, ev, type);
13192
13193        // when done, reset the started state
13194        if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) {
13195            this.started = false;
13196        }
13197
13198        this.callback(this.manager, type, {
13199            pointers: touches[0],
13200            changedPointers: touches[1],
13201            pointerType: INPUT_TYPE_TOUCH,
13202            srcEvent: ev
13203        });
13204    }
13205});
13206
13207/**
13208 * @this {TouchInput}
13209 * @param {Object} ev
13210 * @param {Number} type flag
13211 * @returns {undefined|Array} [all, changed]
13212 */
13213function normalizeSingleTouches(ev, type) {
13214    var all = toArray(ev.touches);
13215    var changed = toArray(ev.changedTouches);
13216
13217    if (type & (INPUT_END | INPUT_CANCEL)) {
13218        all = uniqueArray(all.concat(changed), 'identifier', true);
13219    }
13220
13221    return [all, changed];
13222}
13223
13224var TOUCH_INPUT_MAP = {
13225    touchstart: INPUT_START,
13226    touchmove: INPUT_MOVE,
13227    touchend: INPUT_END,
13228    touchcancel: INPUT_CANCEL
13229};
13230
13231var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel';
13232
13233/**
13234 * Multi-user touch events input
13235 * @constructor
13236 * @extends Input
13237 */
13238function TouchInput() {
13239    this.evTarget = TOUCH_TARGET_EVENTS;
13240    this.targetIds = {};
13241
13242    Input.apply(this, arguments);
13243}
13244
13245inherit(TouchInput, Input, {
13246    handler: function MTEhandler(ev) {
13247        var type = TOUCH_INPUT_MAP[ev.type];
13248        var touches = getTouches.call(this, ev, type);
13249        if (!touches) {
13250            return;
13251        }
13252
13253        this.callback(this.manager, type, {
13254            pointers: touches[0],
13255            changedPointers: touches[1],
13256            pointerType: INPUT_TYPE_TOUCH,
13257            srcEvent: ev
13258        });
13259    }
13260});
13261
13262/**
13263 * @this {TouchInput}
13264 * @param {Object} ev
13265 * @param {Number} type flag
13266 * @returns {undefined|Array} [all, changed]
13267 */
13268function getTouches(ev, type) {
13269    var allTouches = toArray(ev.touches);
13270    var targetIds = this.targetIds;
13271
13272    // when there is only one touch, the process can be simplified
13273    if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) {
13274        targetIds[allTouches[0].identifier] = true;
13275        return [allTouches, allTouches];
13276    }
13277
13278    var i,
13279        targetTouches,
13280        changedTouches = toArray(ev.changedTouches),
13281        changedTargetTouches = [],
13282        target = this.target;
13283
13284    // get target touches from touches
13285    targetTouches = allTouches.filter(function(touch) {
13286        return hasParent(touch.target, target);
13287    });
13288
13289    // collect touches
13290    if (type === INPUT_START) {
13291        i = 0;
13292        while (i < targetTouches.length) {
13293            targetIds[targetTouches[i].identifier] = true;
13294            i++;
13295        }
13296    }
13297
13298    // filter changed touches to only contain touches that exist in the collected target ids
13299    i = 0;
13300    while (i < changedTouches.length) {
13301        if (targetIds[changedTouches[i].identifier]) {
13302            changedTargetTouches.push(changedTouches[i]);
13303        }
13304
13305        // cleanup removed touches
13306        if (type & (INPUT_END | INPUT_CANCEL)) {
13307            delete targetIds[changedTouches[i].identifier];
13308        }
13309        i++;
13310    }
13311
13312    if (!changedTargetTouches.length) {
13313        return;
13314    }
13315
13316    return [
13317        // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel'
13318        uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true),
13319        changedTargetTouches
13320    ];
13321}
13322
13323/**
13324 * Combined touch and mouse input
13325 *
13326 * Touch has a higher priority then mouse, and while touching no mouse events are allowed.
13327 * This because touch devices also emit mouse events while doing a touch.
13328 *
13329 * @constructor
13330 * @extends Input
13331 */
13332
13333var DEDUP_TIMEOUT = 2500;
13334var DEDUP_DISTANCE = 25;
13335
13336function TouchMouseInput() {
13337    Input.apply(this, arguments);
13338
13339    var handler = bindFn(this.handler, this);
13340    this.touch = new TouchInput(this.manager, handler);
13341    this.mouse = new MouseInput(this.manager, handler);
13342
13343    this.primaryTouch = null;
13344    this.lastTouches = [];
13345}
13346
13347inherit(TouchMouseInput, Input, {
13348    /**
13349     * handle mouse and touch events
13350     * @param {NGHammer} manager
13351     * @param {String} inputEvent
13352     * @param {Object} inputData
13353     */
13354    handler: function TMEhandler(manager, inputEvent, inputData) {
13355        var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH),
13356            isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE);
13357
13358        if (isMouse && inputData.sourceCapabilities && inputData.sourceCapabilities.firesTouchEvents) {
13359            return;
13360        }
13361
13362        // when we're in a touch event, record touches to  de-dupe synthetic mouse event
13363        if (isTouch) {
13364            recordTouches.call(this, inputEvent, inputData);
13365        } else if (isMouse && isSyntheticEvent.call(this, inputData)) {
13366            return;
13367        }
13368
13369        this.callback(manager, inputEvent, inputData);
13370    },
13371
13372    /**
13373     * remove the event listeners
13374     */
13375    destroy: function destroy() {
13376        this.touch.destroy();
13377        this.mouse.destroy();
13378    }
13379});
13380
13381function recordTouches(eventType, eventData) {
13382    if (eventType & INPUT_START) {
13383        this.primaryTouch = eventData.changedPointers[0].identifier;
13384        setLastTouch.call(this, eventData);
13385    } else if (eventType & (INPUT_END | INPUT_CANCEL)) {
13386        setLastTouch.call(this, eventData);
13387    }
13388}
13389
13390function setLastTouch(eventData) {
13391    var touch = eventData.changedPointers[0];
13392
13393    if (touch.identifier === this.primaryTouch) {
13394        var lastTouch = {x: touch.clientX, y: touch.clientY};
13395        this.lastTouches.push(lastTouch);
13396        var lts = this.lastTouches;
13397        var removeLastTouch = function() {
13398            var i = lts.indexOf(lastTouch);
13399            if (i > -1) {
13400                lts.splice(i, 1);
13401            }
13402        };
13403        setTimeout(removeLastTouch, DEDUP_TIMEOUT);
13404    }
13405}
13406
13407function isSyntheticEvent(eventData) {
13408    var x = eventData.srcEvent.clientX, y = eventData.srcEvent.clientY;
13409    for (var i = 0; i < this.lastTouches.length; i++) {
13410        var t = this.lastTouches[i];
13411        var dx = Math.abs(x - t.x), dy = Math.abs(y - t.y);
13412        if (dx <= DEDUP_DISTANCE && dy <= DEDUP_DISTANCE) {
13413            return true;
13414        }
13415    }
13416    return false;
13417}
13418
13419var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction');
13420var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined;
13421
13422// magical touchAction value
13423var TOUCH_ACTION_COMPUTE = 'compute';
13424var TOUCH_ACTION_AUTO = 'auto';
13425var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented
13426var TOUCH_ACTION_NONE = 'none';
13427var TOUCH_ACTION_PAN_X = 'pan-x';
13428var TOUCH_ACTION_PAN_Y = 'pan-y';
13429var TOUCH_ACTION_MAP = getTouchActionProps();
13430
13431/**
13432 * Touch Action
13433 * sets the touchAction property or uses the js alternative
13434 * @param {Manager} manager
13435 * @param {String} value
13436 * @constructor
13437 */
13438function TouchAction(manager, value) {
13439    this.manager = manager;
13440    this.set(value);
13441}
13442
13443TouchAction.prototype = {
13444    /**
13445     * set the touchAction value on the element or enable the polyfill
13446     * @param {String} value
13447     */
13448    set: function(value) {
13449        // find out the touch-action by the event handlers
13450        if (value == TOUCH_ACTION_COMPUTE) {
13451            value = this.compute();
13452        }
13453
13454        if (NATIVE_TOUCH_ACTION && this.manager.element.style && TOUCH_ACTION_MAP[value]) {
13455            this.manager.element.style[PREFIXED_TOUCH_ACTION] = value;
13456        }
13457        this.actions = value.toLowerCase().trim();
13458    },
13459
13460    /**
13461     * just re-set the touchAction value
13462     */
13463    update: function() {
13464        this.set(this.manager.options.touchAction);
13465    },
13466
13467    /**
13468     * compute the value for the touchAction property based on the recognizer's settings
13469     * @returns {String} value
13470     */
13471    compute: function() {
13472        var actions = [];
13473        each(this.manager.recognizers, function(recognizer) {
13474            if (boolOrFn(recognizer.options.enable, [recognizer])) {
13475                actions = actions.concat(recognizer.getTouchAction());
13476            }
13477        });
13478        return cleanTouchActions(actions.join(' '));
13479    },
13480
13481    /**
13482     * this method is called on each input cycle and provides the preventing of the browser behavior
13483     * @param {Object} input
13484     */
13485    preventDefaults: function(input) {
13486        var srcEvent = input.srcEvent;
13487        var direction = input.offsetDirection;
13488
13489        // if the touch action did prevented once this session
13490        if (this.manager.session.prevented) {
13491            srcEvent.preventDefault();
13492            return;
13493        }
13494
13495        var actions = this.actions;
13496        var hasNone = inStr(actions, TOUCH_ACTION_NONE) && !TOUCH_ACTION_MAP[TOUCH_ACTION_NONE];
13497        var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y) && !TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_Y];
13498        var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X) && !TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_X];
13499
13500        if (hasNone) {
13501            //do not prevent defaults if this is a tap gesture
13502
13503            var isTapPointer = input.pointers.length === 1;
13504            var isTapMovement = input.distance < 2;
13505            var isTapTouchTime = input.deltaTime < 250;
13506
13507            if (isTapPointer && isTapMovement && isTapTouchTime) {
13508                return;
13509            }
13510        }
13511
13512        if (hasPanX && hasPanY) {
13513            // `pan-x pan-y` means browser handles all scrolling/panning, do not prevent
13514            return;
13515        }
13516
13517        if (hasNone ||
13518            (hasPanY && direction & DIRECTION_HORIZONTAL) ||
13519            (hasPanX && direction & DIRECTION_VERTICAL)) {
13520            return this.preventSrc(srcEvent);
13521        }
13522    },
13523
13524    /**
13525     * call preventDefault to prevent the browser's default behavior (scrolling in most cases)
13526     * @param {Object} srcEvent
13527     */
13528    preventSrc: function(srcEvent) {
13529        this.manager.session.prevented = true;
13530        srcEvent.preventDefault();
13531    }
13532};
13533
13534/**
13535 * when the touchActions are collected they are not a valid value, so we need to clean things up. *
13536 * @param {String} actions
13537 * @returns {*}
13538 */
13539function cleanTouchActions(actions) {
13540    // none
13541    if (inStr(actions, TOUCH_ACTION_NONE)) {
13542        return TOUCH_ACTION_NONE;
13543    }
13544
13545    var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X);
13546    var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y);
13547
13548    // if both pan-x and pan-y are set (different recognizers
13549    // for different directions, e.g. horizontal pan but vertical swipe?)
13550    // we need none (as otherwise with pan-x pan-y combined none of these
13551    // recognizers will work, since the browser would handle all panning
13552    if (hasPanX && hasPanY) {
13553        return TOUCH_ACTION_NONE;
13554    }
13555
13556    // pan-x OR pan-y
13557    if (hasPanX || hasPanY) {
13558        return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y;
13559    }
13560
13561    // manipulation
13562    if (inStr(actions, TOUCH_ACTION_MANIPULATION)) {
13563        return TOUCH_ACTION_MANIPULATION;
13564    }
13565
13566    return TOUCH_ACTION_AUTO;
13567}
13568
13569function getTouchActionProps() {
13570    if (!NATIVE_TOUCH_ACTION) {
13571        return false;
13572    }
13573    var touchMap = {};
13574    var cssSupports = window.CSS && window.CSS.supports;
13575    ['auto', 'manipulation', 'pan-y', 'pan-x', 'pan-x pan-y', 'none'].forEach(function(val) {
13576
13577        // If css.supports is not supported but there is native touch-action assume it supports
13578        // all values. This is the case for IE 10 and 11.
13579        touchMap[val] = cssSupports ? window.CSS.supports('touch-action', val) : true;
13580    });
13581    return touchMap;
13582}
13583
13584/**
13585 * Recognizer flow explained; *
13586 * All recognizers have the initial state of POSSIBLE when a input session starts.
13587 * The definition of a input session is from the first input until the last input, with all it's movement in it. *
13588 * Example session for mouse-input: mousedown -> mousemove -> mouseup
13589 *
13590 * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed
13591 * which determines with state it should be.
13592 *
13593 * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to
13594 * POSSIBLE to give it another change on the next cycle.
13595 *
13596 *               Possible
13597 *                  |
13598 *            +-----+---------------+
13599 *            |                     |
13600 *      +-----+-----+               |
13601 *      |           |               |
13602 *   Failed      Cancelled          |
13603 *                          +-------+------+
13604 *                          |              |
13605 *                      Recognized       Began
13606 *                                         |
13607 *                                      Changed
13608 *                                         |
13609 *                                  Ended/Recognized
13610 */
13611var STATE_POSSIBLE = 1;
13612var STATE_BEGAN = 2;
13613var STATE_CHANGED = 4;
13614var STATE_ENDED = 8;
13615var STATE_RECOGNIZED = STATE_ENDED;
13616var STATE_CANCELLED = 16;
13617var STATE_FAILED = 32;
13618
13619/**
13620 * Recognizer
13621 * Every recognizer needs to extend from this class.
13622 * @constructor
13623 * @param {Object} options
13624 */
13625function Recognizer(options) {
13626    this.options = assign({}, this.defaults, options || {});
13627
13628    this.id = uniqueId();
13629
13630    this.manager = null;
13631
13632    // default is enable true
13633    this.options.enable = ifUndefined(this.options.enable, true);
13634
13635    this.state = STATE_POSSIBLE;
13636
13637    this.simultaneous = {};
13638    this.requireFail = [];
13639}
13640
13641Recognizer.prototype = {
13642    /**
13643     * @virtual
13644     * @type {Object}
13645     */
13646    defaults: {},
13647
13648    /**
13649     * set options
13650     * @param {Object} options
13651     * @return {Recognizer}
13652     */
13653    set: function(options) {
13654        assign(this.options, options);
13655
13656        // also update the touchAction, in case something changed about the directions/enabled state
13657        this.manager && this.manager.touchAction.update();
13658        return this;
13659    },
13660
13661    /**
13662     * recognize simultaneous with an other recognizer.
13663     * @param {Recognizer} otherRecognizer
13664     * @returns {Recognizer} this
13665     */
13666    recognizeWith: function(otherRecognizer) {
13667        if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) {
13668            return this;
13669        }
13670
13671        var simultaneous = this.simultaneous;
13672        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
13673        if (!simultaneous[otherRecognizer.id]) {
13674            simultaneous[otherRecognizer.id] = otherRecognizer;
13675            otherRecognizer.recognizeWith(this);
13676        }
13677        return this;
13678    },
13679
13680    /**
13681     * drop the simultaneous link. it doesnt remove the link on the other recognizer.
13682     * @param {Recognizer} otherRecognizer
13683     * @returns {Recognizer} this
13684     */
13685    dropRecognizeWith: function(otherRecognizer) {
13686        if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) {
13687            return this;
13688        }
13689
13690        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
13691        delete this.simultaneous[otherRecognizer.id];
13692        return this;
13693    },
13694
13695    /**
13696     * recognizer can only run when an other is failing
13697     * @param {Recognizer} otherRecognizer
13698     * @returns {Recognizer} this
13699     */
13700    requireFailure: function(otherRecognizer) {
13701        if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) {
13702            return this;
13703        }
13704
13705        var requireFail = this.requireFail;
13706        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
13707        if (inArray(requireFail, otherRecognizer) === -1) {
13708            requireFail.push(otherRecognizer);
13709            otherRecognizer.requireFailure(this);
13710        }
13711        return this;
13712    },
13713
13714    /**
13715     * drop the requireFailure link. it does not remove the link on the other recognizer.
13716     * @param {Recognizer} otherRecognizer
13717     * @returns {Recognizer} this
13718     */
13719    dropRequireFailure: function(otherRecognizer) {
13720        if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) {
13721            return this;
13722        }
13723
13724        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
13725        var index = inArray(this.requireFail, otherRecognizer);
13726        if (index > -1) {
13727            this.requireFail.splice(index, 1);
13728        }
13729        return this;
13730    },
13731
13732    /**
13733     * has require failures boolean
13734     * @returns {boolean}
13735     */
13736    hasRequireFailures: function() {
13737        return this.requireFail.length > 0;
13738    },
13739
13740    /**
13741     * if the recognizer can recognize simultaneous with an other recognizer
13742     * @param {Recognizer} otherRecognizer
13743     * @returns {Boolean}
13744     */
13745    canRecognizeWith: function(otherRecognizer) {
13746        return !!this.simultaneous[otherRecognizer.id];
13747    },
13748
13749    /**
13750     * You should use `tryEmit` instead of `emit` directly to check
13751     * that all the needed recognizers has failed before emitting.
13752     * @param {Object} input
13753     */
13754    emit: function(input) {
13755        var self = this;
13756        var state = this.state;
13757
13758        function emit(event) {
13759            self.manager.emit(event, input);
13760        }
13761
13762        // 'panstart' and 'panmove'
13763        if (state < STATE_ENDED) {
13764            emit(self.options.event + stateStr(state));
13765        }
13766
13767        emit(self.options.event); // simple 'eventName' events
13768
13769        if (input.additionalEvent) { // additional event(panleft, panright, pinchin, pinchout...)
13770            emit(input.additionalEvent);
13771        }
13772
13773        // panend and pancancel
13774        if (state >= STATE_ENDED) {
13775            emit(self.options.event + stateStr(state));
13776        }
13777    },
13778
13779    /**
13780     * Check that all the require failure recognizers has failed,
13781     * if true, it emits a gesture event,
13782     * otherwise, setup the state to FAILED.
13783     * @param {Object} input
13784     */
13785    tryEmit: function(input) {
13786        if (this.canEmit()) {
13787            return this.emit(input);
13788        }
13789        // it's failing anyway
13790        this.state = STATE_FAILED;
13791    },
13792
13793    /**
13794     * can we emit?
13795     * @returns {boolean}
13796     */
13797    canEmit: function() {
13798        var i = 0;
13799        while (i < this.requireFail.length) {
13800            if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) {
13801                return false;
13802            }
13803            i++;
13804        }
13805        return true;
13806    },
13807
13808    /**
13809     * update the recognizer
13810     * @param {Object} inputData
13811     */
13812    recognize: function(inputData) {
13813        // make a new copy of the inputData
13814        // so we can change the inputData without messing up the other recognizers
13815        var inputDataClone = assign({}, inputData);
13816
13817        // is is enabled and allow recognizing?
13818        if (!boolOrFn(this.options.enable, [this, inputDataClone])) {
13819            this.reset();
13820            this.state = STATE_FAILED;
13821            return;
13822        }
13823
13824        // reset when we've reached the end
13825        if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) {
13826            this.state = STATE_POSSIBLE;
13827        }
13828
13829        this.state = this.process(inputDataClone);
13830
13831        // the recognizer has recognized a gesture
13832        // so trigger an event
13833        if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) {
13834            this.tryEmit(inputDataClone);
13835        }
13836    },
13837
13838    /**
13839     * return the state of the recognizer
13840     * the actual recognizing happens in this method
13841     * @virtual
13842     * @param {Object} inputData
13843     * @returns {Const} STATE
13844     */
13845    process: function(inputData) { }, // jshint ignore:line
13846
13847    /**
13848     * return the preferred touch-action
13849     * @virtual
13850     * @returns {Array}
13851     */
13852    getTouchAction: function() { },
13853
13854    /**
13855     * called when the gesture isn't allowed to recognize
13856     * like when another is being recognized or it is disabled
13857     * @virtual
13858     */
13859    reset: function() { }
13860};
13861
13862/**
13863 * get a usable string, used as event postfix
13864 * @param {Const} state
13865 * @returns {String} state
13866 */
13867function stateStr(state) {
13868    if (state & STATE_CANCELLED) {
13869        return 'cancel';
13870    } else if (state & STATE_ENDED) {
13871        return 'end';
13872    } else if (state & STATE_CHANGED) {
13873        return 'move';
13874    } else if (state & STATE_BEGAN) {
13875        return 'start';
13876    }
13877    return '';
13878}
13879
13880/**
13881 * direction cons to string
13882 * @param {Const} direction
13883 * @returns {String}
13884 */
13885function directionStr(direction) {
13886    if (direction == DIRECTION_DOWN) {
13887        return 'down';
13888    } else if (direction == DIRECTION_UP) {
13889        return 'up';
13890    } else if (direction == DIRECTION_LEFT) {
13891        return 'left';
13892    } else if (direction == DIRECTION_RIGHT) {
13893        return 'right';
13894    }
13895    return '';
13896}
13897
13898/**
13899 * get a recognizer by name if it is bound to a manager
13900 * @param {Recognizer|String} otherRecognizer
13901 * @param {Recognizer} recognizer
13902 * @returns {Recognizer}
13903 */
13904function getRecognizerByNameIfManager(otherRecognizer, recognizer) {
13905    var manager = recognizer.manager;
13906    if (manager) {
13907        return manager.get(otherRecognizer);
13908    }
13909    return otherRecognizer;
13910}
13911
13912/**
13913 * This recognizer is just used as a base for the simple attribute recognizers.
13914 * @constructor
13915 * @extends Recognizer
13916 */
13917function AttrRecognizer() {
13918    Recognizer.apply(this, arguments);
13919}
13920
13921inherit(AttrRecognizer, Recognizer, {
13922    /**
13923     * @namespace
13924     * @memberof AttrRecognizer
13925     */
13926    defaults: {
13927        /**
13928         * @type {Number}
13929         * @default 1
13930         */
13931        pointers: 1
13932    },
13933
13934    /**
13935     * Used to check if it the recognizer receives valid input, like input.distance > 10.
13936     * @memberof AttrRecognizer
13937     * @param {Object} input
13938     * @returns {Boolean} recognized
13939     */
13940    attrTest: function(input) {
13941        var optionPointers = this.options.pointers;
13942        return optionPointers === 0 || input.pointers.length === optionPointers;
13943    },
13944
13945    /**
13946     * Process the input and return the state for the recognizer
13947     * @memberof AttrRecognizer
13948     * @param {Object} input
13949     * @returns {*} State
13950     */
13951    process: function(input) {
13952        var state = this.state;
13953        var eventType = input.eventType;
13954
13955        var isRecognized = state & (STATE_BEGAN | STATE_CHANGED);
13956        var isValid = this.attrTest(input);
13957
13958        // on cancel input and we've recognized before, return STATE_CANCELLED
13959        if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) {
13960            return state | STATE_CANCELLED;
13961        } else if (isRecognized || isValid) {
13962            if (eventType & INPUT_END) {
13963                return state | STATE_ENDED;
13964            } else if (!(state & STATE_BEGAN)) {
13965                return STATE_BEGAN;
13966            }
13967            return state | STATE_CHANGED;
13968        }
13969        return STATE_FAILED;
13970    }
13971});
13972
13973/**
13974 * Pan
13975 * Recognized when the pointer is down and moved in the allowed direction.
13976 * @constructor
13977 * @extends AttrRecognizer
13978 */
13979function PanRecognizer() {
13980    AttrRecognizer.apply(this, arguments);
13981
13982    this.pX = null;
13983    this.pY = null;
13984}
13985
13986inherit(PanRecognizer, AttrRecognizer, {
13987    /**
13988     * @namespace
13989     * @memberof PanRecognizer
13990     */
13991    defaults: {
13992        event: 'pan',
13993        threshold: 10,
13994        pointers: 1,
13995        direction: DIRECTION_ALL
13996    },
13997
13998    getTouchAction: function() {
13999        var direction = this.options.direction;
14000        var actions = [];
14001        if (direction & DIRECTION_HORIZONTAL) {
14002            actions.push(TOUCH_ACTION_PAN_Y);
14003        }
14004        if (direction & DIRECTION_VERTICAL) {
14005            actions.push(TOUCH_ACTION_PAN_X);
14006        }
14007        return actions;
14008    },
14009
14010    directionTest: function(input) {
14011        var options = this.options;
14012        var hasMoved = true;
14013        var distance = input.distance;
14014        var direction = input.direction;
14015        var x = input.deltaX;
14016        var y = input.deltaY;
14017
14018        // lock to axis?
14019        if (!(direction & options.direction)) {
14020            if (options.direction & DIRECTION_HORIZONTAL) {
14021                direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT;
14022                hasMoved = x != this.pX;
14023                distance = Math.abs(input.deltaX);
14024            } else {
14025                direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN;
14026                hasMoved = y != this.pY;
14027                distance = Math.abs(input.deltaY);
14028            }
14029        }
14030        input.direction = direction;
14031        return hasMoved && distance > options.threshold && direction & options.direction;
14032    },
14033
14034    attrTest: function(input) {
14035        return AttrRecognizer.prototype.attrTest.call(this, input) &&
14036            (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input)));
14037    },
14038
14039    emit: function(input) {
14040
14041        this.pX = input.deltaX;
14042        this.pY = input.deltaY;
14043
14044        var direction = directionStr(input.direction);
14045
14046        if (direction) {
14047            input.additionalEvent = this.options.event + direction;
14048        }
14049        this._super.emit.call(this, input);
14050    }
14051});
14052
14053/**
14054 * Pinch
14055 * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out).
14056 * @constructor
14057 * @extends AttrRecognizer
14058 */
14059function PinchRecognizer() {
14060    AttrRecognizer.apply(this, arguments);
14061}
14062
14063inherit(PinchRecognizer, AttrRecognizer, {
14064    /**
14065     * @namespace
14066     * @memberof PinchRecognizer
14067     */
14068    defaults: {
14069        event: 'pinch',
14070        threshold: 0,
14071        pointers: 2
14072    },
14073
14074    getTouchAction: function() {
14075        return [TOUCH_ACTION_NONE];
14076    },
14077
14078    attrTest: function(input) {
14079        return this._super.attrTest.call(this, input) &&
14080            (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN);
14081    },
14082
14083    emit: function(input) {
14084        if (input.scale !== 1) {
14085            var inOut = input.scale < 1 ? 'in' : 'out';
14086            input.additionalEvent = this.options.event + inOut;
14087        }
14088        this._super.emit.call(this, input);
14089    }
14090});
14091
14092/**
14093 * Press
14094 * Recognized when the pointer is down for x ms without any movement.
14095 * @constructor
14096 * @extends Recognizer
14097 */
14098function PressRecognizer() {
14099    Recognizer.apply(this, arguments);
14100
14101    this._timer = null;
14102    this._input = null;
14103}
14104
14105inherit(PressRecognizer, Recognizer, {
14106    /**
14107     * @namespace
14108     * @memberof PressRecognizer
14109     */
14110    defaults: {
14111        event: 'press',
14112        pointers: 1,
14113        time: 251, // minimal time of the pointer to be pressed
14114        threshold: 9 // a minimal movement is ok, but keep it low
14115    },
14116
14117    getTouchAction: function() {
14118        return [TOUCH_ACTION_AUTO];
14119    },
14120
14121    process: function(input) {
14122        var options = this.options;
14123        var validPointers = input.pointers.length === options.pointers;
14124        var validMovement = input.distance < options.threshold;
14125        var validTime = input.deltaTime > options.time;
14126
14127        this._input = input;
14128
14129        // we only allow little movement
14130        // and we've reached an end event, so a tap is possible
14131        if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) {
14132            this.reset();
14133        } else if (input.eventType & INPUT_START) {
14134            this.reset();
14135            this._timer = setTimeoutContext(function() {
14136                this.state = STATE_RECOGNIZED;
14137                this.tryEmit();
14138            }, options.time, this);
14139        } else if (input.eventType & INPUT_END) {
14140            return STATE_RECOGNIZED;
14141        }
14142        return STATE_FAILED;
14143    },
14144
14145    reset: function() {
14146        clearTimeout(this._timer);
14147    },
14148
14149    emit: function(input) {
14150        if (this.state !== STATE_RECOGNIZED) {
14151            return;
14152        }
14153
14154        if (input && (input.eventType & INPUT_END)) {
14155            this.manager.emit(this.options.event + 'up', input);
14156        } else {
14157            this._input.timeStamp = now();
14158            this.manager.emit(this.options.event, this._input);
14159        }
14160    }
14161});
14162
14163/**
14164 * Rotate
14165 * Recognized when two or more pointer are moving in a circular motion.
14166 * @constructor
14167 * @extends AttrRecognizer
14168 */
14169function RotateRecognizer() {
14170    AttrRecognizer.apply(this, arguments);
14171}
14172
14173inherit(RotateRecognizer, AttrRecognizer, {
14174    /**
14175     * @namespace
14176     * @memberof RotateRecognizer
14177     */
14178    defaults: {
14179        event: 'rotate',
14180        threshold: 0,
14181        pointers: 2
14182    },
14183
14184    getTouchAction: function() {
14185        return [TOUCH_ACTION_NONE];
14186    },
14187
14188    attrTest: function(input) {
14189        return this._super.attrTest.call(this, input) &&
14190            (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN);
14191    }
14192});
14193
14194/**
14195 * Swipe
14196 * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction.
14197 * @constructor
14198 * @extends AttrRecognizer
14199 */
14200function SwipeRecognizer() {
14201    AttrRecognizer.apply(this, arguments);
14202}
14203
14204inherit(SwipeRecognizer, AttrRecognizer, {
14205    /**
14206     * @namespace
14207     * @memberof SwipeRecognizer
14208     */
14209    defaults: {
14210        event: 'swipe',
14211        threshold: 10,
14212        velocity: 0.3,
14213        direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL,
14214        pointers: 1
14215    },
14216
14217    getTouchAction: function() {
14218        return PanRecognizer.prototype.getTouchAction.call(this);
14219    },
14220
14221    attrTest: function(input) {
14222        var direction = this.options.direction;
14223        var velocity;
14224
14225        if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) {
14226            velocity = input.overallVelocity;
14227        } else if (direction & DIRECTION_HORIZONTAL) {
14228            velocity = input.overallVelocityX;
14229        } else if (direction & DIRECTION_VERTICAL) {
14230            velocity = input.overallVelocityY;
14231        }
14232
14233        return this._super.attrTest.call(this, input) &&
14234            direction & input.offsetDirection &&
14235            input.distance > this.options.threshold &&
14236            input.maxPointers == this.options.pointers &&
14237            abs(velocity) > this.options.velocity && input.eventType & INPUT_END;
14238    },
14239
14240    emit: function(input) {
14241        var direction = directionStr(input.offsetDirection);
14242        if (direction) {
14243            this.manager.emit(this.options.event + direction, input);
14244        }
14245
14246        this.manager.emit(this.options.event, input);
14247    }
14248});
14249
14250/**
14251 * A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur
14252 * between the given interval and position. The delay option can be used to recognize multi-taps without firing
14253 * a single tap.
14254 *
14255 * The eventData from the emitted event contains the property `tapCount`, which contains the amount of
14256 * multi-taps being recognized.
14257 * @constructor
14258 * @extends Recognizer
14259 */
14260function TapRecognizer() {
14261    Recognizer.apply(this, arguments);
14262
14263    // previous time and center,
14264    // used for tap counting
14265    this.pTime = false;
14266    this.pCenter = false;
14267
14268    this._timer = null;
14269    this._input = null;
14270    this.count = 0;
14271}
14272
14273inherit(TapRecognizer, Recognizer, {
14274    /**
14275     * @namespace
14276     * @memberof PinchRecognizer
14277     */
14278    defaults: {
14279        event: 'tap',
14280        pointers: 1,
14281        taps: 1,
14282        interval: 300, // max time between the multi-tap taps
14283        time: 250, // max time of the pointer to be down (like finger on the screen)
14284        threshold: 9, // a minimal movement is ok, but keep it low
14285        posThreshold: 10 // a multi-tap can be a bit off the initial position
14286    },
14287
14288    getTouchAction: function() {
14289        return [TOUCH_ACTION_MANIPULATION];
14290    },
14291
14292    process: function(input) {
14293        var options = this.options;
14294
14295        var validPointers = input.pointers.length === options.pointers;
14296        var validMovement = input.distance < options.threshold;
14297        var validTouchTime = input.deltaTime < options.time;
14298
14299        this.reset();
14300
14301        if ((input.eventType & INPUT_START) && (this.count === 0)) {
14302            return this.failTimeout();
14303        }
14304
14305        // we only allow little movement
14306        // and we've reached an end event, so a tap is possible
14307        if (validMovement && validTouchTime && validPointers) {
14308            if (input.eventType != INPUT_END) {
14309                return this.failTimeout();
14310            }
14311
14312            var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true;
14313            var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold;
14314
14315            this.pTime = input.timeStamp;
14316            this.pCenter = input.center;
14317
14318            if (!validMultiTap || !validInterval) {
14319                this.count = 1;
14320            } else {
14321                this.count += 1;
14322            }
14323
14324            this._input = input;
14325
14326            // if tap count matches we have recognized it,
14327            // else it has began recognizing...
14328            var tapCount = this.count % options.taps;
14329            if (tapCount === 0) {
14330                // no failing requirements, immediately trigger the tap event
14331                // or wait as long as the multitap interval to trigger
14332                if (!this.hasRequireFailures()) {
14333                    return STATE_RECOGNIZED;
14334                } else {
14335                    this._timer = setTimeoutContext(function() {
14336                        this.state = STATE_RECOGNIZED;
14337                        this.tryEmit();
14338                    }, options.interval, this);
14339                    return STATE_BEGAN;
14340                }
14341            }
14342        }
14343        return STATE_FAILED;
14344    },
14345
14346    failTimeout: function() {
14347        this._timer = setTimeoutContext(function() {
14348            this.state = STATE_FAILED;
14349        }, this.options.interval, this);
14350        return STATE_FAILED;
14351    },
14352
14353    reset: function() {
14354        clearTimeout(this._timer);
14355    },
14356
14357    emit: function() {
14358        if (this.state == STATE_RECOGNIZED) {
14359            this._input.tapCount = this.count;
14360            this.manager.emit(this.options.event, this._input);
14361        }
14362    }
14363});
14364
14365/**
14366 * Simple way to create a manager with a default set of recognizers.
14367 * @param {HTMLElement} element
14368 * @param {Object} [options]
14369 * @constructor
14370 */
14371function NGHammer(element, options) {
14372    options = options || {};
14373    options.recognizers = ifUndefined(options.recognizers, NGHammer.defaults.preset);
14374    return new Manager(element, options);
14375}
14376
14377/**
14378 * @const {string}
14379 */
14380NGHammer.VERSION = '2.0.7';
14381
14382/**
14383 * default settings
14384 * @namespace
14385 */
14386NGHammer.defaults = {
14387    /**
14388     * set if DOM events are being triggered.
14389     * But this is slower and unused by simple implementations, so disabled by default.
14390     * @type {Boolean}
14391     * @default false
14392     */
14393    domEvents: false,
14394
14395    /**
14396     * The value for the touchAction property/fallback.
14397     * When set to `compute` it will magically set the correct value based on the added recognizers.
14398     * @type {String}
14399     * @default compute
14400     */
14401    touchAction: TOUCH_ACTION_COMPUTE,
14402
14403    /**
14404     * @type {Boolean}
14405     * @default true
14406     */
14407    enable: true,
14408
14409    /**
14410     * EXPERIMENTAL FEATURE -- can be removed/changed
14411     * Change the parent input target element.
14412     * If Null, then it is being set the to main element.
14413     * @type {Null|EventTarget}
14414     * @default null
14415     */
14416    inputTarget: null,
14417
14418    /**
14419     * force an input class
14420     * @type {Null|Function}
14421     * @default null
14422     */
14423    inputClass: null,
14424
14425    /**
14426     * Default recognizer setup when calling `NGHammer()`
14427     * When creating a new Manager these will be skipped.
14428     * @type {Array}
14429     */
14430    preset: [
14431        // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...]
14432        [RotateRecognizer, {enable: false}],
14433        [PinchRecognizer, {enable: false}, ['rotate']],
14434        [SwipeRecognizer, {direction: DIRECTION_HORIZONTAL}],
14435        [PanRecognizer, {direction: DIRECTION_HORIZONTAL}, ['swipe']],
14436        [TapRecognizer],
14437        [TapRecognizer, {event: 'doubletap', taps: 2}, ['tap']],
14438        [PressRecognizer]
14439    ],
14440
14441    /**
14442     * Some CSS properties can be used to improve the working of NGHammer.
14443     * Add them to this method and they will be set when creating a new Manager.
14444     * @namespace
14445     */
14446    cssProps: {
14447        /**
14448         * Disables text selection to improve the dragging gesture. Mainly for desktop browsers.
14449         * @type {String}
14450         * @default 'none'
14451         */
14452        userSelect: 'none',
14453
14454        /**
14455         * Disable the Windows Phone grippers when pressing an element.
14456         * @type {String}
14457         * @default 'none'
14458         */
14459        touchSelect: 'none',
14460
14461        /**
14462         * Disables the default callout shown when you touch and hold a touch target.
14463         * On iOS, when you touch and hold a touch target such as a link, Safari displays
14464         * a callout containing information about the link. This property allows you to disable that callout.
14465         * @type {String}
14466         * @default 'none'
14467         */
14468        touchCallout: 'none',
14469
14470        /**
14471         * Specifies whether zooming is enabled. Used by IE10>
14472         * @type {String}
14473         * @default 'none'
14474         */
14475        contentZooming: 'none',
14476
14477        /**
14478         * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers.
14479         * @type {String}
14480         * @default 'none'
14481         */
14482        userDrag: 'none',
14483
14484        /**
14485         * Overrides the highlight color shown when the user taps a link or a JavaScript
14486         * clickable element in iOS. This property obeys the alpha value, if specified.
14487         * @type {String}
14488         * @default 'rgba(0,0,0,0)'
14489         */
14490        tapHighlightColor: 'rgba(0,0,0,0)'
14491    }
14492};
14493
14494var STOP = 1;
14495var FORCED_STOP = 2;
14496
14497/**
14498 * Manager
14499 * @param {HTMLElement} element
14500 * @param {Object} [options]
14501 * @constructor
14502 */
14503function Manager(element, options) {
14504    this.options = assign({}, NGHammer.defaults, options || {});
14505
14506    this.options.inputTarget = this.options.inputTarget || element;
14507
14508    this.handlers = {};
14509    this.session = {};
14510    this.recognizers = [];
14511    this.oldCssProps = {};
14512
14513    this.element = element;
14514    this.input = createInputInstance(this);
14515    this.touchAction = new TouchAction(this, this.options.touchAction);
14516
14517    toggleCssProps(this, true);
14518
14519    each(this.options.recognizers, function(item) {
14520        var recognizer = this.add(new (item[0])(item[1]));
14521        item[2] && recognizer.recognizeWith(item[2]);
14522        item[3] && recognizer.requireFailure(item[3]);
14523    }, this);
14524}
14525
14526Manager.prototype = {
14527    /**
14528     * set options
14529     * @param {Object} options
14530     * @returns {Manager}
14531     */
14532    set: function(options) {
14533        assign(this.options, options);
14534
14535        // Options that need a little more setup
14536        if (options.touchAction) {
14537            this.touchAction.update();
14538        }
14539        if (options.inputTarget) {
14540            // Clean up existing event listeners and reinitialize
14541            this.input.destroy();
14542            this.input.target = options.inputTarget;
14543            this.input.init();
14544        }
14545        return this;
14546    },
14547
14548    /**
14549     * stop recognizing for this session.
14550     * This session will be discarded, when a new [input]start event is fired.
14551     * When forced, the recognizer cycle is stopped immediately.
14552     * @param {Boolean} [force]
14553     */
14554    stop: function(force) {
14555        this.session.stopped = force ? FORCED_STOP : STOP;
14556    },
14557
14558    /**
14559     * run the recognizers!
14560     * called by the inputHandler function on every movement of the pointers (touches)
14561     * it walks through all the recognizers and tries to detect the gesture that is being made
14562     * @param {Object} inputData
14563     */
14564    recognize: function(inputData) {
14565        var session = this.session;
14566        if (session.stopped) {
14567            return;
14568        }
14569
14570        // run the touch-action polyfill
14571        this.touchAction.preventDefaults(inputData);
14572
14573        var recognizer;
14574        var recognizers = this.recognizers;
14575
14576        // this holds the recognizer that is being recognized.
14577        // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED
14578        // if no recognizer is detecting a thing, it is set to `null`
14579        var curRecognizer = session.curRecognizer;
14580
14581        // reset when the last recognizer is recognized
14582        // or when we're in a new session
14583        if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) {
14584            curRecognizer = session.curRecognizer = null;
14585        }
14586
14587        var i = 0;
14588        while (i < recognizers.length) {
14589            recognizer = recognizers[i];
14590
14591            // find out if we are allowed try to recognize the input for this one.
14592            // 1.   allow if the session is NOT forced stopped (see the .stop() method)
14593            // 2.   allow if we still haven't recognized a gesture in this session, or the this recognizer is the one
14594            //      that is being recognized.
14595            // 3.   allow if the recognizer is allowed to run simultaneous with the current recognized recognizer.
14596            //      this can be setup with the `recognizeWith()` method on the recognizer.
14597            if (session.stopped !== FORCED_STOP && ( // 1
14598                    !curRecognizer || recognizer == curRecognizer || // 2
14599                    recognizer.canRecognizeWith(curRecognizer))) { // 3
14600                recognizer.recognize(inputData);
14601            } else {
14602                recognizer.reset();
14603            }
14604
14605            // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the
14606            // current active recognizer. but only if we don't already have an active recognizer
14607            if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) {
14608                curRecognizer = session.curRecognizer = recognizer;
14609            }
14610            i++;
14611        }
14612    },
14613
14614    /**
14615     * get a recognizer by its event name.
14616     * @param {Recognizer|String} recognizer
14617     * @returns {Recognizer|Null}
14618     */
14619    get: function(recognizer) {
14620        if (recognizer instanceof Recognizer) {
14621            return recognizer;
14622        }
14623
14624        var recognizers = this.recognizers;
14625        for (var i = 0; i < recognizers.length; i++) {
14626            if (recognizers[i].options.event == recognizer) {
14627                return recognizers[i];
14628            }
14629        }
14630        return null;
14631    },
14632
14633    /**
14634     * add a recognizer to the manager
14635     * existing recognizers with the same event name will be removed
14636     * @param {Recognizer} recognizer
14637     * @returns {Recognizer|Manager}
14638     */
14639    add: function(recognizer) {
14640        if (invokeArrayArg(recognizer, 'add', this)) {
14641            return this;
14642        }
14643
14644        // remove existing
14645        var existing = this.get(recognizer.options.event);
14646        if (existing) {
14647            this.remove(existing);
14648        }
14649
14650        this.recognizers.push(recognizer);
14651        recognizer.manager = this;
14652
14653        this.touchAction.update();
14654        return recognizer;
14655    },
14656
14657    /**
14658     * remove a recognizer by name or instance
14659     * @param {Recognizer|String} recognizer
14660     * @returns {Manager}
14661     */
14662    remove: function(recognizer) {
14663        if (invokeArrayArg(recognizer, 'remove', this)) {
14664            return this;
14665        }
14666
14667        recognizer = this.get(recognizer);
14668
14669        // let's make sure this recognizer exists
14670        if (recognizer) {
14671            var recognizers = this.recognizers;
14672            var index = inArray(recognizers, recognizer);
14673
14674            if (index !== -1) {
14675                recognizers.splice(index, 1);
14676                this.touchAction.update();
14677            }
14678        }
14679
14680        return this;
14681    },
14682
14683    /**
14684     * bind event
14685     * @param {String} events
14686     * @param {Function} handler
14687     * @returns {EventEmitter} this
14688     */
14689    on: function(events, handler) {
14690        if (events === undefined) {
14691            return;
14692        }
14693        if (handler === undefined) {
14694            return;
14695        }
14696
14697        var handlers = this.handlers;
14698        each(splitStr(events), function(event) {
14699            handlers[event] = handlers[event] || [];
14700            handlers[event].push(handler);
14701        });
14702        return this;
14703    },
14704
14705    /**
14706     * unbind event, leave emit blank to remove all handlers
14707     * @param {String} events
14708     * @param {Function} [handler]
14709     * @returns {EventEmitter} this
14710     */
14711    off: function(events, handler) {
14712        if (events === undefined) {
14713            return;
14714        }
14715
14716        var handlers = this.handlers;
14717        each(splitStr(events), function(event) {
14718            if (!handler) {
14719                delete handlers[event];
14720            } else {
14721                handlers[event] && handlers[event].splice(inArray(handlers[event], handler), 1);
14722            }
14723        });
14724        return this;
14725    },
14726
14727    /**
14728     * emit event to the listeners
14729     * @param {String} event
14730     * @param {Object} data
14731     */
14732    emit: function(event, data) {
14733        // we also want to trigger dom events
14734        if (this.options.domEvents) {
14735            triggerDomEvent(event, data);
14736        }
14737
14738        // no handlers, so skip it all
14739        var handlers = this.handlers[event] && this.handlers[event].slice();
14740        if (!handlers || !handlers.length) {
14741            return;
14742        }
14743
14744        data.type = event;
14745        data.preventDefault = function() {
14746            data.srcEvent.preventDefault();
14747        };
14748
14749        var i = 0;
14750        while (i < handlers.length) {
14751            handlers[i](data);
14752            i++;
14753        }
14754    },
14755
14756    /**
14757     * destroy the manager and unbinds all events
14758     * it doesn't unbind dom events, that is the user own responsibility
14759     */
14760    destroy: function() {
14761        this.element && toggleCssProps(this, false);
14762
14763        this.handlers = {};
14764        this.session = {};
14765        this.input.destroy();
14766        this.element = null;
14767    }
14768};
14769
14770/**
14771 * add/remove the css properties as defined in manager.options.cssProps
14772 * @param {Manager} manager
14773 * @param {Boolean} add
14774 */
14775function toggleCssProps(manager, add) {
14776    var element = manager.element;
14777    if (!element.style) {
14778        return;
14779    }
14780    var prop;
14781    each(manager.options.cssProps, function(value, name) {
14782        prop = prefixed(element.style, name);
14783        if (add) {
14784            manager.oldCssProps[prop] = element.style[prop];
14785            element.style[prop] = value;
14786        } else {
14787            element.style[prop] = manager.oldCssProps[prop] || '';
14788        }
14789    });
14790    if (!add) {
14791        manager.oldCssProps = {};
14792    }
14793}
14794
14795/**
14796 * trigger dom event
14797 * @param {String} event
14798 * @param {Object} data
14799 */
14800function triggerDomEvent(event, data) {
14801    var gestureEvent = document.createEvent('Event');
14802    gestureEvent.initEvent(event, true, true);
14803    gestureEvent.gesture = data;
14804    data.target.dispatchEvent(gestureEvent);
14805}
14806
14807assign(NGHammer, {
14808    INPUT_START: INPUT_START,
14809    INPUT_MOVE: INPUT_MOVE,
14810    INPUT_END: INPUT_END,
14811    INPUT_CANCEL: INPUT_CANCEL,
14812
14813    STATE_POSSIBLE: STATE_POSSIBLE,
14814    STATE_BEGAN: STATE_BEGAN,
14815    STATE_CHANGED: STATE_CHANGED,
14816    STATE_ENDED: STATE_ENDED,
14817    STATE_RECOGNIZED: STATE_RECOGNIZED,
14818    STATE_CANCELLED: STATE_CANCELLED,
14819    STATE_FAILED: STATE_FAILED,
14820
14821    DIRECTION_NONE: DIRECTION_NONE,
14822    DIRECTION_LEFT: DIRECTION_LEFT,
14823    DIRECTION_RIGHT: DIRECTION_RIGHT,
14824    DIRECTION_UP: DIRECTION_UP,
14825    DIRECTION_DOWN: DIRECTION_DOWN,
14826    DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL,
14827    DIRECTION_VERTICAL: DIRECTION_VERTICAL,
14828    DIRECTION_ALL: DIRECTION_ALL,
14829
14830    Manager: Manager,
14831    Input: Input,
14832    TouchAction: TouchAction,
14833
14834    TouchInput: TouchInput,
14835    MouseInput: MouseInput,
14836    PointerEventInput: PointerEventInput,
14837    TouchMouseInput: TouchMouseInput,
14838    SingleTouchInput: SingleTouchInput,
14839
14840    Recognizer: Recognizer,
14841    AttrRecognizer: AttrRecognizer,
14842    Tap: TapRecognizer,
14843    Pan: PanRecognizer,
14844    Swipe: SwipeRecognizer,
14845    Pinch: PinchRecognizer,
14846    Rotate: RotateRecognizer,
14847    Press: PressRecognizer,
14848
14849    on: addEventListeners,
14850    off: removeEventListeners,
14851    each: each,
14852    merge: merge,
14853    extend: extend,
14854    assign: assign,
14855    inherit: inherit,
14856    bindFn: bindFn,
14857    prefixed: prefixed
14858});
14859
14860// this prevents errors when NGHammer is loaded in the presence of an AMD
14861//  style loader but by script tag, not by the loader.
14862var freeGlobal = (typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : {})); // jshint ignore:line
14863freeGlobal.NGHammer = NGHammer;
14864
14865if (typeof define === 'function' && define.amdDISABLED) {
14866    define(function() {
14867        return NGHammer;
14868    });
14869} else if (typeof module != 'undefined' && module.exports) {
14870    module.exports = NGHammer;
14871} else {
14872    window[exportName] = NGHammer;
14873}
14874
14875})(window, document, 'NGHammer');
14876
14877
14878
14879
14880
14881
14882// END NANOGALLERY2
14883// }( jQuery )));
14884}));
14885
14886
14887//##########################################################################################################################
14888//##########################################################################################################################
14889//##########################################################################################################################
14890//##########################################################################################################################
14891//##########################################################################################################################
14892
14893// nanogallery2 auto start whithout javascript call
14894(function(){
14895  'use strict';
vendor: 38,223 bytes, lines 14896-15949
14896  jQuery(document).ready(function () {
14897  
14898    // var t=document.querySelectorAll('[data-nanogallery2-portable]');
14899    // if( t.length > 0 ) {
14900      // portable mode
14901      // var link = document.createElement('link');
14902      // link.setAttribute("rel", "stylesheet");
14903      // link.setAttribute("type", "text/css");
14904      // link.onload = function(){
14905        // for( var i=0; i < t.length; i++ ) {
14906          // jQuery(t[i]).nanogallery2(jQuery(t[i]).data('nanogallery2-portable'));
14907        // }
14908      // }
14909      // link.setAttribute("href", '//nano.gallery/css/nanogallery2.css');
14910      // document.getElementsByTagName("head")[0].appendChild(link);
14911    // }
14912    // else {
14913      // standard mode
14914      var t=document.querySelectorAll('[data-nanogallery2]');
14915      for( var i=0; i < t.length; i++ ) {
14916        jQuery( t[i] ).nanogallery2( jQuery(t[i]).data('nanogallery2') );
14917      }
14918    // }
14919    
14920  });
14921}).call(null);
14922
14923
14924/**!
14925 * @preserve nanogallery2 - NANOPHOTOSPROVIDER2 data provider
14926 * Homepage: http://nanogallery2.nanostudio.org
14927 * Sources:  https://github.com/nanostudio-org/nanogallery2
14928 *
14929 * License:  GPLv3 and commercial licence
14930 * 
14931*/
14932 
14933// ########################################################
14934// ##### nanogallery2 - module NANOPHOTOSPROVIDER2    #####
14935// ########################################################
14936
14937
14938(function (factory) {
14939    "use strict";
14940    if (typeof define === 'function' && define.amd) {
14941        // AMD. Register as an anonymous module.
14942        define(['jquery', 'nanogallery2'], factory);
14943    } else if (typeof exports === 'object' && typeof require === 'function') {
14944        // Browserify
14945        factory(require(['jquery', 'nanogallery2']));
14946    } else {
14947        // Browser globals
14948        factory(jQuery);
14949    }
14950}(function ($) {
14951// ;(function ($) {
14952  
14953  jQuery.nanogallery2.data_nano_photos_provider2 = function (instance, fnName){
14954    var G = instance;      // current nanogallery2 instance
14955    
14956    /** @function AlbumGetContent */
14957    var AlbumGetContent = function(albumID, fnToCall, fnParam1, fnParam2) {
14958
14959      var albumIdx = NGY2Item.GetIdx(G, albumID);
14960
14961      // title is identical to ID (only for albums)
14962      if( instance.I[albumIdx].title == '' ) {
14963        instance.I[albumIdx].title = JsonConvertCharset(albumID);
14964      }
14965
14966      // Build the URL
14967      var url = G.O.dataProvider + '?albumID='+albumID;             // which album
14968      // all thumbnails sizes (for responsive display)
14969      url += '&hxs=' + G.tn.settings.getH(G.GOM.curNavLevel, 'xs');
14970      url += '&wxs=' + G.tn.settings.getW(G.GOM.curNavLevel, 'xs');
14971      url += '&hsm=' + G.tn.settings.getH(G.GOM.curNavLevel, 'sm');
14972      url += '&wsm=' + G.tn.settings.getW(G.GOM.curNavLevel, 'sm');
14973      url += '&hme=' + G.tn.settings.getH(G.GOM.curNavLevel, 'me');
14974      url += '&wme=' + G.tn.settings.getW(G.GOM.curNavLevel, 'me');
14975      url += '&hla=' + G.tn.settings.getH(G.GOM.curNavLevel, 'la');
14976      url += '&wla=' + G.tn.settings.getW(G.GOM.curNavLevel, 'la');
14977      url += '&hxl=' + G.tn.settings.getH(G.GOM.curNavLevel, 'xl');
14978      url += '&wxl=' + G.tn.settings.getW(G.GOM.curNavLevel, 'xl');
14979      // url += '&wxs=' + G.tn.settings.width[G.GOM.curNavLevel].xs;
14980      // url += '&hxs=' + G.tn.settings.height[G.GOM.curNavLevel].xs;
14981      // url += '&wsm=' + G.tn.settings.width[G.GOM.curNavLevel].sm;
14982      // url += '&hsm=' + G.tn.settings.height[G.GOM.curNavLevel].sm;
14983      // url += '&wme=' + G.tn.settings.width[G.GOM.curNavLevel].me;
14984      // url += '&hme=' + G.tn.settings.height[G.GOM.curNavLevel].me;
14985      // url += '&wla=' + G.tn.settings.width[G.GOM.curNavLevel].la;
14986      // url += '&hla=' + G.tn.settings.height[G.GOM.curNavLevel].la;
14987      // url += '&wxl=' + G.tn.settings.width[G.GOM.curNavLevel].xl;
14988      // url += '&hxl=' + G.tn.settings.height[G.GOM.curNavLevel].xl;
14989      
14990      PreloaderDisplay( true );
14991      jQuery.ajaxSetup({ cache: false });
14992      jQuery.support.cors = true;
14993      try {
14994        
14995        var tId = setTimeout( function() {
14996          // workaround to handle JSONP (cross-domain) errors
14997          PreloaderDisplay(false);
14998          NanoAlert(G, 'Could not retrieve nanoPhotosProvider2 data (timeout).');
14999        }, 60000 );
15000
15001        if( G.O.debugMode ) { console.log('nanoPhotosProvider2 URL: ' + url); }
15002        
15003        jQuery.getJSON(url, function(data, status, xhr) {
15004          clearTimeout( tId );
15005          PreloaderDisplay( false );
15006
15007          JsonParseData(albumIdx, data);
15008          
15009          if( data.nano_status == 'ok' ) {
15010            AlbumPostProcess( albumID );
15011            if( fnToCall !== null &&  fnToCall !== undefined) {
15012              fnToCall( fnParam1, fnParam2, null );
15013            }
15014          }
15015          else {
15016            NanoAlert(G, 'Could not retrieve nanoPhotosProvider2 data. Error: ' + data.nano_status + ' - ' + data.nano_message);
15017          }
15018        })
15019        .fail( function(jqxhr, textStatus, error) {
15020          clearTimeout( tId );
15021          PreloaderDisplay( false );
15022
15023          var k=''
15024          for(var key in jqxhr) {
15025            k+= key + '=' + jqxhr[key] +'<br>';
15026          }
15027          var err = textStatus + ', ' + error + ' ' + k + '<br><br>URL:'+url;
15028          NanoAlert(G, 'Could not retrieve nanoPhotosProvider2 data. Error: ' + err);
15029
15030        });    
15031      
15032      }
15033      catch(e) {
15034        NanoAlert(G, 'Could not retrieve nanoPhotosProvider2 data. Error: ' + e);
15035      }
15036    }
15037
15038    
15039    function JsonConvertCharset( str ) {
15040      
15041      return decodeURIComponent(str);
15042
15043
15044      // Pb %C3%A9 --> %E9
15045      // in UTF-8: \u00e9=\xe9 (e9 = hex value)
15046      switch( G.O.dataCharset.toUpperCase() ) {
15047        case 'UTF-8':     // Apache Windows
15048          return decodeURI(str);      // do not use decodeURIComponent (would convert slash also)
15049          break;
15050        case 'Latin':     // Apache Linux
15051        default :
15052          return escape(str);
15053          break;
15054      }
15055    }
15056
15057    function JsonParseData(albumIdx, data) {
15058      if( G.O.debugMode ) { 
15059        console.log('nanoPhotosProvider2 parse data:');
15060        console.dir(data);    
15061      }
15062
15063      var foundAlbumID = false;
15064      var nb = 0;
15065
15066      // loop each item
15067      jQuery.each( data.album_content, function( i, item ){
15068      
15069        // base URL where the images are stored
15070        var baseURL = G.O.dataProvider.substring(0, G.O.dataProvider.indexOf('nano_photos_provider2.php'));
15071        
15072        // image URL
15073        var src = baseURL + JsonConvertCharset( item.src );
15074
15075        // item title
15076        var title = item.title;
15077
15078        // item description ( '_' are replaced with ' ' )
15079        var description = item.description.split('_').join(' ');
15080
15081        // item kind ('album' or 'image')
15082        var kind = 'image';
15083        if( item.kind !== undefined && item.kind.length > 0 ) {
15084          kind = item.kind;
15085        }
15086
15087        // item ID
15088        var ID=item.ID;
15089
15090        var filterAlbum = false;
15091        if( kind == 'album' ) {
15092          // check if 
15093          if( !FilterAlbumName(title, ID) ) { filterAlbum = true; }
15094        }
15095
15096        if( kind == 'image' || (kind == 'album' && FilterAlbumName(title, ID)) ) {
15097          var albumID = 0;
15098          if( item.albumID !== undefined  ) {
15099            albumID = item.albumID;
15100            foundAlbumID = true;
15101          }
15102
15103          var tags = (item.tags === undefined) ? '' : item.tags;
15104          
15105          var newItem = NGY2Item.New( G, title.split('_').join(' ') , description, ID, albumID, kind, tags );
15106          newItem.setMediaURL( src, 'img', description);
15107          
15108          // dominant colorS as a gif
15109          if( item.dcGIF !== undefined ) {
15110            newItem.imageDominantColors='data:image/gif;base64,'+item.dcGIF;
15111          }
15112          // dominant color as hex rgb value
15113          if( item.dc !== undefined && item.dc !== '' ) {
15114            newItem.imageDominantColor=item.dc;
15115          }
15116          
15117          if( kind == 'album' ) {
15118            // number of items in album
15119            newItem.numberItems = item.cnt;
15120          }
15121          else {
15122            // image size
15123            newItem.imageWidth = item.imgWidth;
15124            newItem.imageHeight = item.imgHeight;
15125          }
15126          
15127          // item download URL
15128          if( item.originalURL != '' ) {
15129            newItem.downloadURL = baseURL+JsonConvertCharset(item.originalURL);
15130          }
15131          
15132          // retrieve responsive thumbnails urls and sizes
15133          var cnl = G.GOM.curNavLevel;      // current navigation level ('L1' or 'LN');
15134          var l=['xs', 'sm', 'me', 'la', 'xl'];
15135          for( var n = 0; n < l.length; n++ ) {
15136            newItem.thumbs.url[cnl][l[n]]     = baseURL + JsonConvertCharset(item.t_url[n]);
15137            newItem.thumbs.width[cnl][l[n]]   = parseInt(item.t_width[n]);
15138            newItem.thumbs.height[cnl][l[n]]  = parseInt(item.t_height[n]);
15139          }
15140         
15141          // post-process callback
15142          var fu = G.O.fnProcessData;
15143          if( fu !== null ) {
15144            typeof fu == 'function' ? fu(newItem, G.O.dataProvider, data) : window[fu](newItem, G.O.dataProvider, data);
15145          }
15146          
15147        }
15148      });
15149
15150      G.I[albumIdx].contentIsLoaded = true;   // album's content is ready
15151    }    
15152    
15153
15154    // -----------
15155    // Initialize 
15156    function Init() {
15157
15158    }
15159    
15160
15161    // shortcuts to NGY2Tools functions (with context)
15162    var PreloaderDisplay = NGY2Tools.PreloaderDisplay.bind(G);
15163    // var NanoAlert = NGY2Tools.NanoAlert.bind(G);
15164    var NanoAlert = NGY2Tools.NanoAlert;
15165    var GetImageTitleFromURL = NGY2Tools.GetImageTitleFromURL.bind(G);
15166    var FilterAlbumName = NGY2Tools.FilterAlbumName.bind(G);
15167    var AlbumPostProcess = NGY2Tools.AlbumPostProcess.bind(G);
15168 
15169    switch( fnName ){
15170      case 'GetHiddenAlbums':
15171        break;
15172      case 'AlbumGetContent':
15173        var albumID = arguments[2],
15174        callback = arguments[3],
15175        cbParam1 = arguments[4],
15176        cbParam2 = arguments[5];
15177        AlbumGetContent(albumID, callback, cbParam1, cbParam2);
15178        break;
15179      case 'Init':
15180        Init();
15181        break;
15182      case '':
15183        break;
15184    }
15185
15186  };
15187  
15188// END NANOPHOTOSPROVIDER DATA SOURCE FOR NANOGALLERY2
15189// }( jQuery ));
15190}));  
15191  
15192  
15193  
15194  
15195/**!
15196 * @preserve nanogallery2 - GOOGLE PHOTOS data provider
15197 * Homepage: http://nanogallery2.nanostudio.org
15198 * Sources:  https://github.com/nanostudio-org/nanogallery2
15199 *
15200 * License:  GPLv3 and commercial licence
15201 * 
15202*/
15203 
15204// ###################################################
15205// ##### nanogallery2 - module for GOOGLE PHOTOS #####
15206// ##### requires nanogp                         #####
15207// ###################################################
15208
15209
15210(function (factory) {
15211    "use strict";
15212    if (typeof define === 'function' && define.amd) {
15213        // AMD. Register as an anonymous module.
15214        define(['jquery', 'nanogallery2'], factory);
15215    } else if (typeof exports === 'object' && typeof require === 'function') {
15216        // Browserify
15217        factory(require(['jquery', 'nanogallery2']));
15218    } else {
15219        // Browser globals
15220        factory(jQuery);
15221    }
15222}(function ($) {
15223// ;(function ($) {
15224  
15225  jQuery.nanogallery2.data_google2 = function (instance, fnName){
15226    var G=instance;      // current nanogallery2 instance
15227
15228    
15229    /** @function AlbumGetContent */
15230    var AlbumGetContent = function(albumID, fnToCall, fnParam1, fnParam2) {
15231
15232      var url = '';
15233      var kind = 'image';
15234      var albumIdx = NGY2Item.GetIdx(G, albumID);
15235
15236      var maxResults='';
15237      if( G.galleryMaxItems.Get() > 0 ) {
15238        maxResults = '&max-results=' + G.galleryMaxItems.Get();
15239      }
15240      
15241      var gat='';   // global authorization (using the BUILDER)
15242      if( typeof ngy2_pwa_at !== 'undefined' ) {
15243        gat=ngy2_pwa_at;
15244      }
15245      
15246      if( albumID == 0 ) {
15247        // RETRIEVE THE LIST OF ALBUMS
15248        if( gat != '' ) {
15249          // in builder
15250          // url += '?alt=json&v=3&kind=album&deprecation-extension=true&thumbsize='+G.picasa.thumbSizes+maxResults+'&rnd=' + (new Date().getTime()) + '&access_token=' + gat;
15251          url = 'https://photoslibrary.googleapis.com/v1/albums';
15252        }
15253        else {
15254          // NANOGP2
15255          // url=G.O.google2URL + '?nguserid='+G.O.userID+'&alt=json&v=3&kind=album&thumbsize='+G.picasa.thumbSizes+maxResults+'&rnd=' + (new Date().getTime());
15256          url = G.O.google2URL + '?nguserid=' + G.O.userID + '&alt=json&v=3&kind=album' + maxResults + '&rnd=' + (new Date().getTime());
15257        }
15258        kind='album';
15259
15260      }
15261      else {
15262        // RETRIEVE THE CONTENT OF ONE ALBUM (=MEDIAS)
15263        if( gat != '' ) {
15264          // in builder
15265          // url += '/albumid/'+albumID+'?alt=json&kind=photo&deprecation-extension=true&thumbsize='+G.picasa.thumbSizes+maxResults+'&imgmax=d&access_token=' + gat;
15266          // url += '/albumid/'+albumID+'?alt=json&kind=photo&deprecation-extension=true&thumbsize='+G.picasa.thumbSizes+maxResults+'&imgmax=d&access_token=' + gat;
15267          url = 'https://photoslibrary.googleapis.com/v1/mediaItems:search';
15268        }
15269        else {
15270            // nanogp
15271            // url = G.O.google2URL + '?nguserid='+G.O.userID+'&ngalbumid='+albumID+'&alt=json&v=3&kind=photo&thumbsize='+G.picasa.thumbSizes+maxResults+'&imgmax=d';
15272            url = G.O.google2URL + '?nguserid=' + G.O.userID + '&ngalbumid=' + albumID + '&alt=json&v=3&kind=photo&' + maxResults;
15273        }
15274      }
15275
15276      if( G.O.debugMode ) { console.log('Google Photos URL: ' + url); }
15277      
15278      PreloaderDisplay(true);
15279      jQuery.ajaxSetup({ cache: false });
15280      jQuery.support.cors = true;
15281      try {
15282        var tId = setTimeout( function() {
15283          // workaround to handle JSONP (cross-domain) errors
15284          PreloaderDisplay(false);
15285          NanoAlert('Could not retrieve AJAX data...');
15286        }, 60000 );
15287
15288        jQuery.getJSON( url + '&callback=?', function(data) {
15289
15290          if( data.nano_status == 'error' ) {
15291            clearTimeout(tId);
15292            PreloaderDisplay(false);
15293            NanoAlert(G, "Could not retrieve Google data. Error: " + data.nano_message);
15294            return;
15295          }
15296          clearTimeout(tId);
15297          PreloaderDisplay(false);
15298          GoogleParseData( albumIdx, kind, data );
15299          AlbumPostProcess(albumID);
15300          if( fnToCall !== null &&  fnToCall !== undefined) {
15301            fnToCall( fnParam1, fnParam2, null );
15302          }
15303          
15304        })
15305        .fail( function(jqxhr, textStatus, error) {
15306          clearTimeout(tId);
15307          PreloaderDisplay(false);
15308
15309          var k=''
15310          for(var key in jqxhr) {
15311            k+= key + '=' + jqxhr[key] +'<br>';
15312          }
15313          var err = textStatus + ', ' + error + ' ' + k + '<br><br>URL:'+url;
15314          NanoAlert(G, "Could not retrieve Google data. Error: " + err);
15315        });
15316      }
15317      catch(e) {
15318        NanoAlert(G, "Could not retrieve Google data. Error: " + e);
15319      }
15320    }
15321
15322    
15323    // -----------
15324    // Retrieve items from a Google Photos data stream
15325    // items can be images/viedos or albums
15326    function GoogleParseData(albumIdx, kind, data) {
15327
15328      if( G.O.debugMode ) { 
15329        console.log('Google Photos data:');
15330        console.dir(data);    
15331      }
15332      var albumID = G.I[albumIdx].GetID();
15333
15334      // iterate and parse each item
15335      jQuery.each(data, function(i,data){
15336      
15337        if( typeof data === 'object' && data !== null ) {   // only objects
15338
15339        var itemDescription = '';
15340        var itemTitle = '';
15341        if( kind == 'image') {
15342            itemTitle = data.description;
15343        }
15344        else {
15345          itemTitle = data.title;
15346        }
15347        if( itemTitle == undefined ) {
15348          // may happen...
15349          itemTitle = '';
15350        }
15351        
15352        var itemID = data.id;
15353        if( kind == 'album' ) {
15354          if( !FilterAlbumName(itemTitle, itemID) || data.coverPhotoBaseUrl == undefined ) {
15355            return true;
15356          }
15357        }
15358
15359        // create ngy2 item
15360        var newItem = NGY2Item.New( G, itemTitle, itemDescription, itemID, albumID, kind, '' );
15361        
15362        var width = 0;
15363        var height = 0;
15364        
15365        // set the image src
15366        var src = '';
15367        if( kind == 'image' ) {
15368          src = data.baseUrl;
15369          if( !G.O.viewerZoom && G.O.viewerZoom != undefined ) {
15370            if( window.screen.width >  window.screen.height ) {
15371              src += '=w' + window.screen.width;
15372            }
15373            else {
15374              src = s + '=h' + window.screen.height;
15375            }
15376          }
15377          else {
15378            // use full resolution image
15379            src += '=h' + data.mediaMetadata.height + '-w' + data.mediaMetadata.width;
15380            
15381            // use original image
15382            // src += '=d';
15383          }
15384          
15385          // image's URL
15386          newItem.setMediaURL( src, 'img', description);
15387
15388          // image size
15389          if( data.mediaMetadata.width !== undefined ) {
15390            newItem.imageWidth = parseInt(data.mediaMetadata.width);
15391            width = newItem.imageWidth;
15392          }
15393          if( data.mediaMetadata.height !== undefined ) {
15394            newItem.imageHeight=parseInt(data.mediaMetadata.height);
15395            height = newItem.imageHeight;
15396          }
15397
15398          // if( data.media$group != null && data.media$group.media$credit != null && data.media$group.media$credit.length > 0 ) {
15399            // newItem.author=data.media$group.media$credit[0].$t;
15400          // }
15401
15402          // Photo
15403          if( data.mediaMetadata.photo !== undefined ) {
15404            // exif data
15405            if( data.mediaMetadata.photo.exposureTime != undefined ) {
15406              newItem.exif.exposure = data.mediaMetadata.photo.exposureTime;
15407            }
15408            if( data.mediaMetadata.photo.focalLength != undefined ) {
15409              newItem.exif.focallength = data.mediaMetadata.photo.focalLength;
15410            }
15411            if( data.mediaMetadata.photo.apertureFNumber != undefined ) {
15412              newItem.exif.fstop = data.mediaMetadata.photo.apertureFNumber;
15413            }
15414            if( data.mediaMetadata.photo.isoEquivalent != undefined ) {
15415              newItem.exif.iso = data.mediaMetadata.photo.isoEquivalent;
15416            }
15417            if( data.mediaMetadata.photo.cameraModel != undefined ) {
15418              newItem.exif.model = data.mediaMetadata.photo.cameraModel;
15419            }
15420          }
15421          
15422          // Video
15423          if( data.mediaMetadata.video !== undefined ) {
15424            if( data.mediaMetadata.video.cameraModel != undefined ) {
15425              newItem.exif.model = data.mediaMetadata.video.cameraModel;
15426            }
15427            
15428            newItem.downloadURL = data.baseUrl + '=dv';   // set the download URL for the video
15429            
15430            // newItem.mediaKind = 'selfhosted';
15431            // newItem.mediaMarkup = '<video controls class="nGY2ViewerMedia"><source src="'+ newItem.src +'" type="video/'+ 'video/mp4' +'" preload="auto">Your browser does not support the video tag (HTML 5).</video>';
15432          }
15433            
15434        }
15435        else {
15436          // newItem.author = data.author[0].name.$t;
15437          newItem.numberItems = data.mediaItemsCount;
15438        }
15439
15440        // set the URL of the thumbnails images
15441        newItem.thumbs=GoogleThumbSetSizes2('l1', newItem.thumbs, data, kind, height, width );
15442        newItem.thumbs=GoogleThumbSetSizes2('lN', newItem.thumbs, data, kind,height ,width );
15443        
15444        // post-process callback
15445        var fu = G.O.fnProcessData;
15446        if( fu !== null ) {
15447          typeof fu == 'function' ? fu(newItem, 'google2', data) : window[fu](newItem, 'google2', data);
15448        }
15449          
15450        }
15451      });
15452
15453      G.I[albumIdx].contentIsLoaded = true;   // album's content is ready
15454    }
15455
15456    // -----------
15457    // Set thumbnail sizes (width and height) and URLs (for all resolutions (xs, sm, me, la, xl) and levels (l1, lN)
15458    function GoogleThumbSetSizes2(level, tn, data, kind, height, width ) {
15459      var sizes=['xs','sm','me','la','xl'];
15460
15461      for(var i=0; i<sizes.length; i++ ) {
15462        
15463        // media
15464        if( kind == 'image' ) {
15465          if( G.tn.settings.width[level][sizes[i]] == 'auto' ) {
15466            var ratio1 = width / height;
15467            tn.height[level][sizes[i]] = G.tn.settings.getH(level, sizes[i]);
15468            tn.width[level][sizes[i]] = G.tn.settings.getH(level, sizes[i]) * ratio1;
15469            tn.url[level][sizes[i]] = data.baseUrl + '=h' + G.tn.settings.getH(level, sizes[i]);
15470            continue;
15471          }
15472          if( G.tn.settings.height[level][sizes[i]] == 'auto' ) {
15473            var ratio1 = height / width;
15474            tn.width[level][sizes[i]] = G.tn.settings.getW(level, sizes[i]);
15475            tn.height[level][sizes[i]] = G.tn.settings.getW(level, sizes[i]) * ratio1;
15476            tn.url[level][sizes[i]] = data.baseUrl + '=w' + G.tn.settings.getW(level, sizes[i]);
15477            continue;
15478          }
15479
15480          tn.height[level][sizes[i]] = G.tn.settings.getH(level, sizes[i]);
15481          tn.width[level][sizes[i]] = G.tn.settings.getW(level, sizes[i]);
15482          tn.url[level][sizes[i]] = data.baseUrl + '=w' + G.tn.settings.getW(level, sizes[i]);
15483          
15484        }
15485
15486        // album
15487        if( kind == 'album' ) {
15488          if( G.tn.settings.width[level][sizes[i]] == 'auto' ) {
15489            tn.url[level][sizes[i]]= data.coverPhotoBaseUrl + '=h' + G.tn.settings.getH(level, sizes[i]);
15490            continue;
15491          }
15492          if( G.tn.settings.height[level][sizes[i]] == 'auto' ) {
15493            tn.url[level][sizes[i]]= data.coverPhotoBaseUrl + '=w' + G.tn.settings.getW(level, sizes[i]);
15494            continue;
15495          }
15496          var w=G.tn.settings.mosaic[level + 'Factor']['w'][sizes[i]];
15497          tn.url[level][sizes[i]]= data.coverPhotoBaseUrl + '=h' + G.tn.settings.getH(level, sizes[i]) + '-w' + G.tn.settings.getW(level, sizes[i]);
15498
15499        }
15500      }
15501        
15502      return tn;
15503    }
15504
15505
15506
15507    // -----------
15508    // Initialization
15509    function Init() {
15510    }
15511    
15512
15513    // shortcuts to NGY2Tools functions (with context)
15514    var PreloaderDisplay = NGY2Tools.PreloaderDisplay.bind(G);
15515    // var NanoAlert = NGY2Tools.NanoAlert.bind(G);
15516    var NanoAlert = NGY2Tools.NanoAlert;
15517    var GetImageTitleFromURL = NGY2Tools.GetImageTitleFromURL.bind(G);
15518    var FilterAlbumName = NGY2Tools.FilterAlbumName.bind(G);
15519    var AlbumPostProcess = NGY2Tools.AlbumPostProcess.bind(G);
15520 
15521    switch( fnName ){
15522      case 'AlbumGetContent':
15523        var albumID = arguments[2],
15524        callback2 = arguments[3],
15525        cbParam1 = arguments[4],
15526        cbParam2 = arguments[5];
15527        AlbumGetContent(albumID, callback2, cbParam1, cbParam2);
15528        break;
15529      case 'Init':
15530        Init();
15531        break;
15532      case '':
15533        break;
15534    }
15535
15536  };
15537  
15538// END GOOGLE DATA SOURCE FOR NANOGALLERY2
15539// }( jQuery ));
15540}));
15541  
15542  
15543  
15544  
15545/**!
15546 * @preserve nanogallery2 - FLICKR data provider
15547 * Homepage: http://nanogallery2.nanostudio.org
15548 * Sources:  https://github.com/nanostudio-org/nanogallery2
15549 *
15550 * License:  GPLv3 and commercial licence
15551 * 
15552*/
15553 
15554// ############################################
15555// ##### nanogallery2 - module for FLICKR #####
15556// ############################################
15557
15558
15559(function (factory) {
15560    "use strict";
15561    if (typeof define === 'function' && define.amd) {
15562        // AMD. Register as an anonymous module.
15563        define(['jquery', 'nanogallery2'], factory);
15564    } else if (typeof exports === 'object' && typeof require === 'function') {
15565        // Browserify
15566        factory(require(['jquery', 'nanogallery2']));
15567    } else {
15568        // Browser globals
15569        factory(jQuery);
15570    }
15571}(function ($) {
15572// ;(function ($) {
15573  
15574  jQuery.nanogallery2.data_flickr = function (instance, fnName){
15575    var G = instance;      // current nanogallery2 instance
15576
15577    // ### Flickr
15578    // Details: http://www.flickr.com/services/api/misc.urls.html
15579    var Flickr = {
15580      url: function() {
15581        // Flickr API Going SSL-Only on June 27th, 2014
15582        return 'https://api.flickr.com/services/rest/';
15583      },
15584      thumbSize:'               sq',
15585      thumbAvailableSizes :     new Array(75, 100, 150, 240, 500, 640),
15586      thumbAvailableSizesStr :  new Array('sq', 't', 'q', 's', 'm', 'z'),
15587      photoSize :               '0',
15588      photoAvailableSizes :     new Array(75, 100, 150, 240, 500, 640, 1024, 1024, 1600, 2048, 10000),
15589      photoAvailableSizesStr :  new Array('sq', 't', 'q', 's', 'm', 'z', 'b', 'l', 'h', 'k', 'o'),
15590      ApiKey :                  "2f0e634b471fdb47446abcb9c5afebdc"
15591    };
15592    
15593    
15594    /** @function AlbumGetContent */
15595    var AlbumGetContent = function(albumID, fnToCall, fnParam1, fnParam2) {
15596
15597      var albumIdx = NGY2Item.GetIdx(G, albumID);
15598      var url = '';
15599      var kind = 'image';
15600        // photos
15601        if( G.O.photoset.toUpperCase() == 'NONE' || G.O.album.toUpperCase() == 'NONE' ) {
15602          // get photos from full photostream
15603          url = Flickr.url() + "?&method=flickr.people.getPublicPhotos&api_key=" + Flickr.ApiKey + "&user_id="+G.O.userID+"&extras=description,views,tags,url_o,url_sq,url_t,url_q,url_s,url_m,url_z,url_b,url_h,url_k&per_page=500&format=json";
15604        }
15605        else
15606          if( G.I[albumIdx].GetID() == 0 ) {
15607          // retrieve the list of albums
15608          url = Flickr.url() + "?&method=flickr.photosets.getList&api_key=" + Flickr.ApiKey + "&user_id="+G.O.userID+"&per_page=500&primary_photo_extras=tags,url_o,url_sq,url_t,url_q,url_s,url_m,url_l,url_z,url_b,url_h,url_k&format=json";
15609          kind='album';
15610        }
15611          else {
15612            // photos from one specific photoset
15613            url = Flickr.url() + "?&method=flickr.photosets.getPhotos&api_key=" + Flickr.ApiKey + "&photoset_id="+G.I[albumIdx].GetID()+"&extras=description,views,tags,url_o,url_sq,url_t,url_q,url_s,url_m,url_l,url_z,url_b,url_h,url_k&format=json";
15614          }
15615
15616      if( G.O.debugMode ) { console.log('Flickr URL: ' + url); }
15617          
15618      PreloaderDisplay(true);
15619      jQuery.ajaxSetup({ cache: false });
15620      jQuery.support.cors = true;
15621      
15622      var tId = setTimeout( function() {
15623        // workaround to handle JSONP (cross-domain) errors
15624        PreloaderDisplay(false);
15625        NanoAlert(G, 'Could not retrieve AJAX data...');
15626      }, 60000 );
15627      
15628      var sourceData=[];
15629
15630      // Process the downloaded data
15631      var FlickrGetDone = function() {
15632        clearTimeout(tId);
15633        PreloaderDisplay(false);
15634        
15635        if( kind == 'album' ) {
15636          FlickrParsePhotoSets(albumIdx, albumID, sourceData);
15637        }
15638        else {
15639          FlickrParsePhotos(albumIdx, albumID, sourceData);
15640        }
15641        
15642        AlbumPostProcess(albumID);
15643        if( fnToCall !== null &&  fnToCall !== undefined) {
15644          fnToCall( fnParam1, fnParam2, null );
15645        }
15646      }
15647      
15648      // download one page of data (=500 entries)
15649      var FlickrGetOnePage = function( url, page ) {
15650        jQuery.getJSON( url + '&page=' + page + '&jsoncallback=?', function(data, status, xhr) {
15651
15652          var pages=0;
15653          if( kind == 'album' ) {
15654            if( data.stat !== undefined && data.stat === 'fail' ) {
15655              NanoAlert(G, "Could not retrieve Flickr album list: " + data.message + " (code: "+data.code+").");
15656              return false;
15657            }
15658            sourceData=sourceData.concat(data.photosets.photoset);
15659            pages=data.photosets.pages;
15660          }
15661          else {
15662            if( G.O.photoset.toUpperCase() == 'NONE' || G.O.album.toUpperCase() == 'NONE' ) {
15663              // content of full photoset
15664              sourceData=sourceData.concat(data.photos.photo);
15665              pages=data.photos.pages;
15666            }
15667            else {
15668              // content of one album
15669              if( data.stat !== undefined && data.stat === 'fail' ) {
15670                NanoAlert(G, "Could not retrieve Flickr album: " + data.message + " (code: "+data.code+").");
15671                return false;
15672              }
15673              if( G.I[albumIdx].title == '' ) {
15674                G.I[albumIdx].title=data.photoset.title;
15675              }
15676              sourceData=sourceData.concat(data.photoset.photo);
15677              pages=data.photoset.pages;
15678            }
15679            
15680          }
15681          
15682          if( pages > page ) {
15683            FlickrGetOnePage(url, page+1);
15684          }
15685          else {
15686            FlickrGetDone();
15687          }
15688        })
15689        .fail( function(jqxhr, textStatus, error) {
15690          clearTimeout(tId);
15691          PreloaderDisplay(false);
15692          NanoAlert(G, "Could not retrieve Flickr ajax data: " + textStatus + ', ' + error);
15693        });
15694
15695      }
15696      
15697      FlickrGetOnePage(url, 1);
15698      
15699    }
15700
15701
15702    
15703    // -----------
15704    // Retrieve items for one Flickr photoset
15705    function FlickrParsePhotos( albumIdx, albumID, source ) {
15706
15707      if( G.O.debugMode ) { 
15708        console.log('Flickr parse photos:');
15709        console.dir(source);    
15710      }
15711      
15712      jQuery.each(source, function(i,item){
15713
15714        var itemID = item.id;
15715
15716        var imgUrl=item.url_sq;  //fallback size
15717
15718        // get the title
15719        var itemTitle = item.title;
15720        if( G.O.thumbnailLabel.get('title') != '' ) {
15721          itemTitle=GetImageTitleFromURL(imgUrl);
15722        }
15723
15724        // get the description
15725        var itemDescription=item.description._content;
15726        
15727        // retrieve the image size with highest available resolution
15728        var imgW=75, imgH=75;
15729        var start=Flickr.photoAvailableSizesStr.length-1;
15730        if( G.O.flickrSkipOriginal ) { start--; }
15731        for( var i = start; i>=0 ; i-- ) {
15732          if( item['url_'+Flickr.photoAvailableSizesStr[i]] != undefined ) {
15733            imgUrl=item['url_'+Flickr.photoAvailableSizesStr[i]];
15734            imgW=parseInt(item['width_'+Flickr.photoAvailableSizesStr[i]]);
15735            imgH=parseInt(item['height_'+Flickr.photoAvailableSizesStr[i]]);
15736            break;
15737          }
15738        }
15739
15740        var sizes = {};
15741        for( var p in item ) {
15742          if( p.indexOf('height_') == 0 || p.indexOf('width_') == 0 || p.indexOf('url_') == 0 ) {
15743            sizes[p]=item[p];
15744          }
15745        }
15746        
15747        // tags
15748        var tags = item.tags !== undefined ? item.tags : '';
15749
15750        // create item
15751        var newItem = NGY2Item.New( G, itemTitle, itemDescription, itemID, albumID, 'image', tags );
15752
15753        // add image
15754        newItem.setMediaURL( imgUrl, 'img', description);
15755        newItem.imageWidth = imgW;
15756        newItem.imageHeight = imgH;
15757
15758        
15759        // add thumbnails
15760        var tn = {
15761          url:    { l1 : { xs:'', sm:'', me:'', la:'', xl:'' }, lN : { xs:'', sm:'', me:'', la:'', xl:'' } },
15762          width:  { l1 : { xs:0, sm:0, me:0, la:0, xl:0 }, lN : { xs:0, sm:0, me:0, la:0, xl:0 } },
15763          height: { l1 : { xs:0, sm:0, me:0, la:0, xl:0 }, lN : { xs:0, sm:0, me:0, la:0, xl:0 } }
15764        };
15765        tn=FlickrRetrieveImages(tn, item, 'l1' );
15766        tn=FlickrRetrieveImages(tn, item, 'lN' );
15767        newItem.thumbs=tn;
15768        
15769        // post-process callback
15770        var fu = G.O.fnProcessData;
15771        if( fu !== null ) {
15772          typeof fu == 'function' ? fu(newItem, 'flickr', item) : window[fu](newItem, 'flickr', item);
15773        }
15774        
15775
15776      });
15777      G.I[albumIdx].contentIsLoaded=true;
15778      
15779    }    
15780
15781
15782    
15783    // -----------
15784    // Retrieve the list of Flickr photosets
15785    function FlickrParsePhotoSets( albumIdx, albumID, source ) {
15786
15787      if( G.O.debugMode ) { 
15788        console.log('Flickr parse list of albums:');
15789        console.dir(source);    
15790      }
15791
15792      jQuery.each(source, function(i,item){
15793        //Get the title
15794        var itemTitle = item.title._content;
15795        
15796        if( item.visibility_can_see_set == 0 ) { return true; }    // skip it
15797        
15798        if( FilterAlbumName(itemTitle, item.id) ) {
15799          var itemID=item.id;
15800          //Get the description
15801          var itemDescription = item.description._content != undefined ? item.description._content : '';
15802
15803          var sizes = {};
15804          for( var p in item.primary_photo_extras) {
15805            sizes[p] = item.primary_photo_extras[p];
15806          }
15807          var tags='';
15808          if( item.primary_photo_extras !== undefined ) {
15809            if( item.primary_photo_extras.tags !== undefined ) {
15810              tags = item.primary_photo_extras.tags;
15811            }
15812          }
15813        
15814          var newItem = NGY2Item.New( G, itemTitle, itemDescription, itemID, albumID, 'album', tags );
15815          newItem.numberItems = item.photos;
15816          newItem.thumbSizes = sizes;
15817          
15818          var tn = {
15819            url:    { l1 : { xs:'', sm:'', me:'', la:'', xl:'' }, lN : { xs:'', sm:'', me:'', la:'', xl:'' } },
15820            width:  { l1 : { xs:0, sm:0, me:0, la:0, xl:0 }, lN : { xs:0, sm:0, me:0, la:0, xl:0 } },
15821            height: { l1 : { xs:0, sm:0, me:0, la:0, xl:0 }, lN : { xs:0, sm:0, me:0, la:0, xl:0 } }
15822          };
15823          tn = FlickrRetrieveImages(tn, item.primary_photo_extras, 'l1' );
15824          tn = FlickrRetrieveImages(tn, item.primary_photo_extras, 'lN' );
15825          newItem.thumbs = tn;
15826
15827          // post-process callback
15828          var fu = G.O.fnProcessData;
15829          if( fu !== null ) {
15830            typeof fu == 'function' ? fu(newItem, 'flickr', item) : window[fu](newItem, 'flickr', item);
15831          }
15832          
15833        }
15834      });
15835      
15836      G.I[albumIdx].contentIsLoaded=true;
15837    }
15838
15839    function FlickrRetrieveImages(tn, item, level ) {
15840
15841      var sf=1;
15842      if( G.tn.opt[level].crop === true ) {
15843        sf=G.O.thumbnailCropScaleFactor;
15844      }
15845    
15846    
15847      var sizes=['xs','sm','me','la','xl'];
15848      for( var i=0; i<sizes.length; i++ ) {
15849        if( G.tn.settings.width[level][sizes[i]] == 'auto' || G.tn.settings.width[level][sizes[i]] == '' ) {
15850          var sdir='height_';
15851          var tsize=Math.ceil( G.tn.settings.height[level][sizes[i]] * G.tn.scale * sf * G.tn.settings.mosaic[level+'Factor']['h'][sizes[i]] );
15852          var one=FlickrRetrieveOneImage(sdir, tsize, item );
15853          tn.url[level][sizes[i]]=one.url;
15854          tn.width[level][sizes[i]]=one.width;
15855          tn.height[level][sizes[i]]=one.height;
15856        }
15857        else 
15858          if( G.tn.settings.height[level][sizes[i]] == 'auto' || G.tn.settings.height[level][sizes[i]] == '' ) {
15859            var sdir='width_';
15860            var tsize=Math.ceil( G.tn.settings.width[level][sizes[i]] * G.tn.scale * sf * G.tn.settings.mosaic[level+'Factor']['w'][sizes[i]] );
15861            var one=FlickrRetrieveOneImage(sdir, tsize, item );
15862            tn.url[level][sizes[i]]=one.url;
15863            tn.width[level][sizes[i]]=one.width;
15864            tn.height[level][sizes[i]]=one.height;
15865          }
15866          else {
15867            var sdir='height_';
15868            var tsize=Math.ceil( G.tn.settings.height[level][sizes[i]] * G.tn.scale * sf * G.tn.settings.mosaic[level+'Factor']['h'][sizes[i]] );
15869            if( G.tn.settings.width[level][sizes[i]] > G.tn.settings.height[level][sizes[i]] ) {
15870              sdir='width_';
15871              tsize=Math.ceil( G.tn.settings.width[level][sizes[i]] * G.tn.scale * sf * G.tn.settings.mosaic[level+'Factor']['w'][sizes[i]] );
15872            }
15873            var one=FlickrRetrieveOneImage(sdir, tsize, item );
15874            tn.url[level][sizes[i]]=one.url;
15875            tn.width[level][sizes[i]]=one.width;
15876            tn.height[level][sizes[i]]=one.height;
15877          }
15878      }
15879      return tn;
15880    }
15881    
15882    function FlickrRetrieveOneImage(sdir, tsize, item ) {
15883      var one={ url: '', width: 0, height: 0 };
15884      var tnIndex=0;
15885      for( var j=0; j < Flickr.thumbAvailableSizes.length; j++ ) {
15886        var size=item[sdir+Flickr.photoAvailableSizesStr[j]];
15887        if( size != undefined ) {
15888          tnIndex=j;
15889          if( size >= tsize ) {
15890            break;
15891          }
15892        }
15893      }
15894      var fSize=Flickr.photoAvailableSizesStr[tnIndex];
15895      one.url = item['url_'+fSize];
15896      one.width = parseInt(item['width_'+fSize]);
15897      one.height = parseInt(item['height_'+fSize]);
15898      return one;
15899    }    
15900
15901    
15902    /** @function GetHiddenAlbums */
15903    var GetHiddenAlbums = function( hiddenAlbums, callback ){
15904      // not supported -> doesn't exit in Flickr
15905      callback();     
15906    }
15907
15908    // -----------
15909    // Initialize thumbnail sizes
15910    function Init() {
15911      return;
15912    }
15913
15914
15915    // shortcuts to NGY2Tools functions (with context)
15916    var PreloaderDisplay = NGY2Tools.PreloaderDisplay.bind(G);
15917    var NanoAlert = NGY2Tools.NanoAlert;
15918    var GetImageTitleFromURL = NGY2Tools.GetImageTitleFromURL.bind(G);
15919    var FilterAlbumName = NGY2Tools.FilterAlbumName.bind(G);
15920    var AlbumPostProcess = NGY2Tools.AlbumPostProcess.bind(G);
15921
15922    switch( fnName ){
15923      case 'GetHiddenAlbums':
15924        var hiddenAlbums = arguments[2],
15925        callback = arguments[3];
15926        GetHiddenAlbums(hiddenAlbums, callback);
15927        break;
15928      case 'AlbumGetContent':
15929        var albumID = arguments[2],
15930        callback = arguments[3],
15931        cbParam1 = arguments[4],
15932        cbParam2 = arguments[5];
15933        AlbumGetContent(albumID, callback, cbParam1, cbParam2);
15934        break;
15935      case 'Init':
15936        Init();
15937        break;
15938      case '':
15939        break;
15940    }
15941
15942  };
15943  
15944// END FLICKR DATA SOURCE FOR NANOGALLERY2
15945// }( jQuery ));
15946}));
15947
15948  
15949  

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.