_ _    _ _____  ___   __                       
 __      _(_) | _(_)___ / ( _ ) / /_   ___ ___  _ __ ___  
 \ \ /\ / / | |/ / | |_ \ / _ \| '_ \ / __/ _ \| '_ ` _ \ 
  \ V  V /| |   <| |___) | (_) | (_) | (_| (_) | | | | | |
   \_/\_/ |_|_|\_\_|____/ \___/ \___(_)___\___/|_| |_| |_|

MediaWiki:Gadget-ImageStackPopup.js

In this article, we will delve into the fascinating world of MediaWiki:Gadget-ImageStackPopup.js, exploring its origins, evolution and relevance today. From its appearance in history to its impact on contemporary society, MediaWiki:Gadget-ImageStackPopup.js has played a fundamental role in different aspects of our lives. Along these lines, we will delve into its most relevant and transcendental aspects, analyzing its influence in various areas and its importance in popular culture. Join us on this journey of discovery and reflection about MediaWiki:Gadget-ImageStackPopup.js, an exciting topic that will not leave anyone indifferent.
/******************************************************************************/
/**** THIS PAGE TRACKS ]. PLEASE AVOID EDITING DIRECTLY. 
/**** EDITS SHOULD BE PROPOSED DIRECTLY to ].
/**** A BOT WILL RAISE AN EDIT REQUEST IF IT BECOMES DIFFERENT FROM UPSTREAM.
/******************************************************************************/

// Script written by Bawolff for WikiProject Med Foundation based on earlier ImageStack script by Hellerhoff.
var ImageStackPopup = {

	messages: {
		en: {
			ImageStackPopupFrameBack: 'Back',
			ImageStackPopupFrameImageCredit: 'View media credits',
			ImageStackPopupNextImage: "Next image",
			ImageStackPopupPreviousImage: "Previous image",
			ImageStackPopupSliderLabel: "Select image",
			ImageStackPopupPlayLabel: "Show slideshow",
			ImageStackPopupLoading: "Loading... $1%"
		},
	},

	init: function () {
		ImageStackPopup.setMessages();
		mw.hook( 'wikipage.content' ).add( ImageStackPopup.addPlayButton );
	},

	/**
	 * Set the interface messages in the most appropriate language
	 *
	 * Favor the user language first, the page language second, the wiki language third, and lastly English
	 */
	setMessages: function () {
		var userLanguage = mw.config.get( 'wgUserLanguage' );
		if ( userLanguage in ImageStackPopup.messages ) {
			mw.messages.set( ImageStackPopup.messages );
			return;
		}
		var pageLanguage = mw.config.get( 'wgPageContentLanguage' );
		if ( pageLanguage in ImageStackPopup.messages ) {
			mw.messages.set( ImageStackPopup.messages );
			return;
		}
		var contentLanguage = mw.config.get( 'wgContentLanguage' );
		if ( contentLanguage in ImageStackPopup.messages ) {
			mw.messages.set( ImageStackPopup.messages );
			return;
		}
		mw.messages.set( ImageStackPopup.messages.en );
	},

	/**
	 * Append a play button ► to every ImageStackPopup div
 	 */
	addPlayButton: function ( $content ) {
		$content.find( 'div.ImageStackPopup' ).each( function () {
			var $frame = $( this );
			var viewerInfo = $frame.data( 'imagestackpopupConfig' );
			if ( !( viewerInfo instanceof Array) ) {
				return;
			}
			// match both img and span for broken files in galleries
			$frame.find( '.mw-file-element, .lazy-image-placeholder' ).each( function ( i ) {
				if ( viewerInfo instanceof Object && typeof viewerInfo.list === "string" ) {
					var $play = $( '<button></button>' )
						.attr( {
							type: 'button',
							"class": 'ImageStackPopup-play',
							title: mw.msg( 'ImageStackPopupPlayLabel' ),
							"aria-label": mw.msg( 'ImageStackPopupPlayLabel' )
						} ).text( '►' );
					var data = viewerInfo;
					$play.on( 'click', data, ImageStackPopup.showFrame );
					var $this = $( this );
					$this.parent().css( {display: 'inline-block', height: 'fit-content', position: 'relative' } );
					$this.after( $play );
				}
			} );
		} );
	},

	showFrame: function ( event ) {
		event.preventDefault();
		var data = event.data;

		var $loading = $( '#ImageStackPopupLoading' );
		if ( !$loading.length ) {
				$loading = $( '<div></div>' )
					.attr( {
						id: "ImageStackPopupLoading",
						role: "status"
					}
				);
				$( document.body ).append( $loading );
		}
		$loading.text( mw.msg( 'ImageStackPopupLoading', "0" ) );
		// Load dependencies
		var state = mw.loader.getState( 'oojs-ui-windows' );
		if ( state === 'registered' ) {
			mw.loader.using( 'oojs-ui-windows', function () { ImageStackPopup.showFrame( event ) } );
			return;
		}
		var $viewer = ImageStackPopup.getViewer();

		var config = {
			size: 'full',
			// This doesn't seem to work.
			classes: 'ImageStackPopupDialog',
			title: typeof data.title === 'string' ? data.title : false,
			actions: [ {
				action: 'accept',
				label: mw.msg( 'ImageStackPopupFrameBack' ),
				flags: 
			} ],
			message: $viewer
		};

		var dialog = function ( config ) {
			dialog.super.call( this, config );
			this.$element.addClass( 'ImageStackPopupDialog' );
		}

		OO.inheritClass( dialog, OO.ui.MessageDialog );
		dialog.static.name = 'ImageStack'
		OO.ui.getWindowManager().addWindows(  );
		// copied from OO.ui.alert definition.
		OO.ui.getWindowManager().openWindow( 'ImageStack', config )
			.closed.done( function () {
				// There has to be a better way to do this.
				if ( window.ImageStackPopupCancel ) {
					window.ImageStackPopupCancel();
				}
			});
		ImageStackPopup.loadImages( $viewer, data );
	},

	getViewer: function () {
		var $viewer = $( '<div></div>' ).attr( {
			class: 'ImageStackPopup-viewer ImageStackPopup-loading'
		} );
		// From https://commons.wikimedia.orgview_image.php?q=MediaWiki:Gadget-ImageStackPopup.js&sq=MediaWiki:Gadget-ImageStackPopup.js&lang=en&file=File:Loading_spinner.svg
		$viewer.append( '<svg xmlns="http://www.w3.org/2000/svg" aria-label="Loading..." viewBox="0 0 100 100" width="25%" height="25%" style="display:block;margin:auto"><rect fill="#555" height="6" opacity=".083" rx="3" ry="3" transform="rotate(-60 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".167" rx="3" ry="3" transform="rotate(-30 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".25" rx="3" ry="3" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".333" rx="3" ry="3" transform="rotate(30 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".417" rx="3" ry="3" transform="rotate(60 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".5" rx="3" ry="3" transform="rotate(90 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".583" rx="3" ry="3" transform="rotate(120 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".667" rx="3" ry="3" transform="rotate(150 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".75" rx="3" ry="3" transform="rotate(180 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".833" rx="3" ry="3" transform="rotate(210 50 50)" width="25" x="72" y="47"/><rect fill="#555" height="6" opacity=".917" rx="3" ry="3" transform="rotate(240 50 50)" width="25" x="72" y="47"/></svg>' );
		return $viewer;
	},

	loadImages: function ( $viewer, data ) {
		var page = mw.Title.newFromText( data.list );
		if ( !page ) {
			console.log( "Image stack error, invalid page " + data.list );
			return;
		}

		fetch( page.getUrl() )
			.then( function ( response ) { return response.text() } )
			.then( function ( text ) { return ImageStackPopup.handlePage( $viewer, data, text ) } );
	},

	handlePage: function( $viewer, data, text ) {
		var parser = new DOMParser;
		var listDoc = parser.parseFromString( text, 'text/html' );
		var idSelector = mw.Title.newFromText( data.list ).getFragment();

		var listElm = listDoc.getElementById( idSelector );
		if ( !listElm ) {
			console.log( "Error finding element in list document" );
			return;
		}

		var imgs = listElm.querySelectorAll( 'img.mw-file-element' );

		var width = imgs.width;
		var height = imgs.height;

		var context = new ImageStackPopup.Context( $viewer, data, imgs, width, height );
	},

	getSource: function ( imgElm, width, height ) {
		// desired dimensions
		var w = width * window.devicePixelRatio;
		var h = height * window.devicePixelRatio;
		// current candidate
		var imgW = parseInt(imgElm.width);
		var imgH = parseInt(imgElm.height);
		// img tag width/height.
		var originalW = imgW;
		var originalH = imgH;
		var src = imgElm.src;
		if ( imgW >= w && imgH >= h ) {
			return src;
		}
		var srcSets = imgElm.srcset.split( /\s*,\s*/ );
		for ( var i = 0; i < srcSets.length; i++ ) {
			var parts = srcSets.match( /^(\S+)\s+()x\s*$/ );
			if (
				parts &&
				parts.length === 3
			) {
				var pixelRatio = parseFloat( parts );
				if (
					( imgW < w && originalW*pixelRatio > imgW ) ||
					( imgW > w && originalW*pixelRatio - w >= 0 && originalW*pixelRatio < imgW )
				) {
					imgW = originalW*pixelRatio;
					imgH = originalH*pixelRatio;
					src = parts;
				}
			}
		}
		return src;
	},

	doStats: function () {
		if ( window.imageStackPopupStatsAlreadyDone !== true ) {
			window.imageStackPopupStatsAlreadyDone = true;
			mw.track( 'counter.gadget_ImageStackPopup._all' );
			mw.track( 'counter.gadget_ImageStackPopup.' + mw.config.get( 'wgDBname' ) + '_all' );
			var statName = mw.config.get( 'wgDBname' ) + '_' + mw.config.get( 'wgPageName' );
			statName = encodeURIComponent( statName );
			// Symbols don't seem to work.
			statName = statName.replace( //g, '_' );
			mw.track( 'counter.gadget_ImageStackPopup.' + statName );
		}
	},

	Context: function ( $viewer, config, imgs, width, height ) {
		ImageStackPopup.doStats();
		this.$viewer = $viewer;
		this.loop = !!config.loop;
		this.start = typeof config.start === 'number' ? config.start - 1 : 0;
		this.urls = null;
		this.infoUrls = null;
		this.imgs = imgs;
		this.captionId = typeof config.caption === 'string' ? config.caption : false;
		// Future TODO - make the size of image adaptive to screen size
		// Future TODO - handle images of different sizes and aspect ratios.
		this.width = config.width;
		this.height = config.height;
		if ( this.width && !this.height ) {
			this.height = this.width * (imgs.height)/(imgs.width);
		}
		if ( !this.width && this.height ) {
			this.width = this.height * (imgs.width)/(imgs.height);
		}
		this.imgWidth = width;
		this.imgHeight = height;
		this.currentImage = this.start;
		this.pendingFrame = false;
		this.$loading = $( '#ImageStackPopupLoading' );
		this.urlsLoaded = 0;
		this.pendingTouches = {};

		this.init();
	}
};

// This part is based on Hellerhoff's https://commons.wikimedia.orghttps://wiki386.com/en/MediaWiki:Gadget-ImageStack.js
ImageStackPopup.Context.prototype = {
	init: function () {
		var that = this;
		// Chrome scrolls much faster than firefox
		const SCROLL_SLOWDOWN = navigator.userAgent.includes( "Chrome/" ) ? 25 : 2;
		this.pendingScrollDelta = 0;

		var containingWidth = this.$viewer.parentElement.parentElement.parentElement.clientWidth;
		var containingHeight = this.$viewer.parentElement.parentElement.parentElement.clientHeight;
		this.$viewer.empty();
		$counter = $('<div class="ImageStackCounter">');
		this.$leftLink = $('<a>', {
			href: '#',
			text: '← ',
			title: mw.msg( 'ImageStackPopupPreviousImage' ),
			"aria-label": mw.msg( 'ImageStackPopupPreviousImage' ),
		}).click(function() {
			that.currentImage--;
			that.repaint();
			return false;
		});
		this.$rightLink = $('<a>', {
			href: '#',
			text: ' →',
			title: mw.msg( 'ImageStackPopupNextImage' ),
			"aria-label": mw.msg( 'ImageStackPopupNextImage' ),
		}).click(function() {
			that.currentImage++;
			that.repaint();
			return false;
		});

		this.$slider = $( '<input>', {
			type: 'range',
			min: 0,
			max: that.imgs.length - 1,
			value: this.currentImage,
			"aria-label": mw.msg( 'ImageStackPopupSliderLabel' ),
			class: 'ImageStackPopupSlider'
		} ).on( 'input', function (e) {
			that.currentImage = parseInt( e.target.value );
			that.repaint();
		} ).on( 'keydown', function (e) {
			// Hacky fix. Not enough browsers support the direction: css
			// keyword, so we fix up events here.
			if ( e.key === 'ArrowUp' ) {
				e.preventDefault();
				that.currentImage--;
				that.repaint();
			} else if ( e.key === 'ArrowDown' ) {
				e.preventDefault();
				that.currentImage++;
				that.repaint();
			}
		} );

		var handleTouchStart = this.handleTouchStart.bind(this);
		var handleTouchMove = this.handleTouchMove.bind(this);
		var handleTouchCancel = this.handleTouchCancel.bind(this);
		var handleTouchEnd = this.handleTouchEnd.bind(this);
		var touchElement = this.$viewer.parentElement.parentElement;
		var opt = { passive: true };

		// For now it seems like we don't have to cancel events. Unclear if we should
		touchElement.addEventListener( 'touchstart', handleTouchStart, opt );
		touchElement.addEventListener( 'touchmove', handleTouchMove, opt );
		touchElement.addEventListener( 'touchend', handleTouchEnd, opt );
		touchElement.addEventListener( 'touchcancel', handleTouchCancel, opt );

		var keyeventhandler = this.handleArrow.bind(this);
		document.addEventListener( 'keydown', keyeventhandler );
		// Hacky!
		window.ImageStackPopupCancel = function () {
			document.removeEventListener( 'keydown', keyeventhandler );
			touchElement.removeEventListener( 'touchstart', handleTouchStart, opt );
			touchElement.removeEventListener( 'touchmove', handleTouchMove, opt );
			touchElement.removeEventListener( 'touchend', handleTouchEnd, opt );
			touchElement.removeEventListener( 'touchcancel', handleTouchCancel, opt );
		};
		this.$currentCount = $('<span>', {
			'class': 'ImageStackCounterCurrent',
			text: that.currentImage + 1
		});
		var left = $( '<span>', { class: "ImageStackPopupCounterHideMobile" } ).append( this.$leftLink, '(' );
		var right = $( '<span>', { class: "ImageStackPopupCounterHideMobile" } ).append( ')', this.$rightLink );
		$counter.append(left, this.$currentCount, '/', that.imgs.length, right);
		this.$leftLink.add(this.$rightLink).css({
			fontSize: "110%",
			fontweight: "bold"
		});

		this.img = new Image();
		this.img.fetchPriority = 'high';
		this.img.loading = 'eager';
		this.img.decoding = 'sync';
		this.img.className = 'ImageStackPopupImg';
		// width/height set later.
		var $img = $( this.img );
		$img.on('mousewheel', function(event, delta) {
			// Scroll is too fast (Esp. on chrome), so we buffer scroll events.
			that.pendingScrollDelta += delta;
			var realDelta = Math.floor(that.pendingScrollDelta/SCROLL_SLOWDOWN);
			if (delta !== 0) {
				// We reverse the direction of scroll.
				that.currentImage -= realDelta > 2 ? 2 : realDelta;
				that.pendingScrollDelta -= realDelta*SCROLL_SLOWDOWN;
				that.repaint();
			}
			return false;
		});
		$img.on('mousedown', function(event) { // prepare scroll by drag
			mouse_y = event.screenY; // remember mouse-position
			that.scrollobject = true; // set flag
			return false;
		});
		$img.on('mousemove', function(event) {
			if (that.scrollobject && Math.abs(mouse_y - event.screenY) > 10) {
				var offset = (mouse_y < event.screenY) ? 1 : -1;
				mouse_y = event.screenY; //  remember mouse-position for next event
				that.currentImage += offset;
				that.repaint();
			}
			return false;
		});

		this.img.addEventListener( 'load', this.urlLoaded.bind( this ), { once: true } );
		this.img.addEventListener( 'error', this.urlLoaded.bind( this ), { once: true } );

		var $container = $( '<div class="ImageStackPopupImgContainer"></div>' )
			.append( $counter )
			.append( this.$slider )
			.append( $img );

		this.$viewer.append( $container );
		this.$credit = $( '<a></a>' );
		this.$credit.text( mw.msg( 'ImageStackPopupFrameImageCredit' ) );
		var $creditDiv = $( '<div class="ImageStackPopupCredit"></div>' ).append( this.$credit );
		this.$viewer.append( $creditDiv );
		var $wrapper = false;
		if ( this.captionId ) {
			var captionElm = document.getElementById( this.captionId );
			if ( captionElm ) {
				var newCaption = $( captionElm ).clone();
				newCaption.show();
				$wrapper = $( '<div class="ImageStackPopup-caption"></div>' ).append( newCaption );
				this.$viewer.append( $wrapper );
			}
		}
		// Try to adjust image size to viewer window
		// but do not go so far that the image is blurry
		if ( !this.width ) {
			var controlHeight = $creditDiv.clientHeight;
			var paddingDivStyles = getComputedStyle( this.$viewer.parentElement.parentElement );
			controlHeight += parseFloat( paddingDivStyles.getPropertyValue( 'padding-top' ) ) + parseFloat( paddingDivStyles.getPropertyValue( 'padding-bottom' ) );
			containingWidth -= parseFloat( paddingDivStyles.getPropertyValue( 'padding-left' ) ) + parseFloat( paddingDivStyles.getPropertyValue( 'padding-right' ) );
			if ( $wrapper ) {
				controlHeight += $wrapper.clientHeight;
			}
			controlHeight += 5; // fudge factor.
			if ( this.$viewer.parentElement.previousElementSibling ) {
				// OOUI window label. This is a bit hacky.
				controlHeight += this.$viewer.parentElement.previousElementSibling.clientHeight;
			}
			var maxImgDim = this.getMaxImgDim();
			var aspect = maxImgDim/maxImgDim;
			containingHeight -= controlHeight;
			// 3 to account for slider and text controls. but not on narrow screens.
			if ( containingWidth >= 500 ) {
				containingWidth -= parseFloat( getComputedStyle( this.$slider ).getPropertyValue( 'width' ) ) * 3;
			}

			if ( maxImgDim > maxImgDim ) {
				if ( maxImgDim > containingWidth ) {
					// shrink to fit.
					maxImgDim = containingWidth;
					maxImgDim = Math.floor(containingWidth/aspect);
				}
				if ( maxImgDim > containingHeight ) {
					maxImgDim = containingHeight;
					maxImgDim = Math.floor( containingHeight * aspect );
				}
			} else {
				if ( maxImgDim > containingHeight ) {
					maxImgDim = containingHeight;
					maxImgDim = Math.floor( containingHeight * aspect );
				}
				if ( maxImgDim > containingWidth ) {
					// shrink to fit.
					maxImgDim = containingWidth;
					maxImgDim = Math.floor(containingWidth/aspect);
				}
			}
			this.width = maxImgDim;
			this.height = maxImgDim;
		}
		this.img.width = this.width;
		this.img.height = this.height;
		// different font size in credit div, so don't use em.
		var sliderRoom;
		if ( containingWidth >= 500 ) {
			sliderRoom = parseFloat( getComputedStyle( this.$slider ).getPropertyValue( 'width' ) ) * 3;
		} else {
			sliderRoom = 0;
		}
		$creditDiv.css( 'width', this.width + sliderRoom + 'px' );
		$creditDiv.css( 'padding-right', sliderRoom + 'px' );
		$container.css( 'width', 'calc( ' + this.width + 'px' + ' + 3em )' );
		this.$slider.css( 'height', this.height + 'px' );
		$counter.css( 'min-height', this.height + 'px' );

		this.getUrls();
		this.toggleImg();
		this.preload();
	},

	getMaxImgDim: function () {
		// This assumes that even on high-DPI displays, enlarging to 96dpi is ok.
		var w = this.imgs.width;
		var h = this.imgs.height;
		if ( this.imgs.srcset.match( /\s2x\s*(,|$)/ ) ) {
			w *= 2;
			h *= 2;
		} else if ( this.imgs.srcset.match( /\s1.5x\s*(,|$)/ ) ) {
			w = Math.floor( 1.5*w );
			h = Math.floor( 1.5*h );
		}
		return ;
	},

	repaint: function () {
		if ( this.pendingFrame ) {
			return;
		}
		requestAnimationFrame( this.toggleImg.bind( this ) );
	},

	toggleImg: function () {
		if ( this.loop ) {
			if ( this.currentImage < 0 ) {
				this.currentImage = this.urls.length - 1;
			} else if ( this.currentImage >= this.urls.length ) {
				this.currentImage = 0;
			}
		} else {
			this.$rightLink.css( 'visibility', 'visible' );
			this.$leftLink.css( 'visibility', 'visible' );
			if ( this.currentImage <= 0 ) {
				this.currentImage = 0;
				this.$leftLink.css( 'visibility', 'hidden' );
			} else if ( this.currentImage >= this.urls.length - 1 ) {
				this.currentImage = this.urls.length - 1;
				this.$rightLink.css( 'visibility', 'hidden' );
			}
		}
		this.$slider.value = this.currentImage;
		// Future todo might be to localize digits.
		this.$currentCount.textContent = this.currentImage + 1;
		this.img.src = this.urls;
		this.$credit.href = this.infoUrls;
		if ( this.infoUrls === false ) {
			this.$credit.css( 'visibility', 'hidden' );
		} else {
			this.$credit.css( 'visibility', 'visible' );
		}
		this.pendingFrame = false;
	},
	
	preload: function () {
		for ( var i = 0; i < this.urls.length; i++ ) {
			if ( i === this.currentImage ) {
				// already fetched.
				continue;
			}
			var img = new Image();
			if ( Math.abs( this.currentImage - i ) > 4 ) {
				img.fetchPriority = 'low';
			}
			img.loading = 'eager';
			img.decoding = 'sync';
			img.addEventListener( 'load', this.urlLoaded.bind( this ), { once: true } );
			img.addEventListener( 'error', this.urlLoaded.bind( this ), { once: true} );
			img.src = this.urls;
		}

	},

	getUrls: function () {
		this.urls = ;
		this.infoUrls = ;
		for( var i = 0; i < this.imgs.length; i++ ) {
			this.urls = ImageStackPopup.getSource( this.imgs, this.width, this.height );
			if ( this.imgs.parentElement.href ) {
				this.infoUrls = this.imgs.parentElement.href;
			} else {
				this.infoUrls = false;
			}
		}
	},

	urlLoaded: function () {
		// For now, this still increments for failed loads, so
		// as not to have the progress bar stuck.
		this.urlsLoaded++;
		var progress = Math.floor( ( this.urlsLoaded / this.urls.length ) * 100 );
		if ( this.$loading.length ) {
			this.$loading.text( mw.msg( 'ImageStackPopupLoading', progress ) );
			if ( this.urlsLoaded === this.urls.length ) {
				this.$viewer.removeClass( 'ImageStackPopup-loading' );
				this.$loading.remove();
			}
		}
	},
	
	handleArrow: function (e) {
		// Not sure if we should prevent default here
		// possible accessibility issue if there is somehow something scrollable.
		// in theory, nothing here should be scrollable so it shouldn't matter.
		if (
			( e.key === 'ArrowUp' ||
			e.key === 'ArrowDown' ||
			e.key === 'ArrowRight' ||
			e.key === 'ArrowLeft' )
			&& e.target.tagName !== 'INPUT' 
			&& this.$viewer.find(e)
		) {
			if ( e.key === 'ArrowUp' || e.key === 'ArrowRight' ) {
				this.currentImage--;
				this.repaint();
			} else if ( e.key === 'ArrowDown' || e.key === 'ArrowLeft' ) {
				this.currentImage++;
				this.repaint();
			}
		}
	},

	handleTouchStart: function (e) {
		for ( var i = 0; i < e.changedTouches.length; i++ ) {
			var t = e.changedTouches;
			this.pendingTouches = ;
		}
	},
	handleTouchCancel: function (e) {
		for ( var i = 0; i < e.changedTouches.length; i++ ) {
			var t = e.changedTouches;
			delete this.pendingTouches;
		}
	},
	handleTouchMove: function (e) {
		for ( var i = 0; i < e.changedTouches.length; i++ ) {
			var t = e.changedTouches;
			if ( !this.pendingTouches ) {
				continue;
			}
			var startX = this.pendingTouches;
			var startY = this.pendingTouches;
			var angle = Math.abs( Math.atan( ( startY - t.clientY ) / ( startX - t.clientX ) ) );

			if ( angle > 1 ) {
				// vertical. > ~60 degrees
				if ( Math.abs( startY - t.clientY ) < 15 ) {
					// Not large enough
					continue;
				}
				// reset calculation so we move image if they move 15 more pixels
				this.pendingTouches = ;
				if ( startY - t.clientY > 0 ) {
					// swipe up
					this.currentImage--;
					this.repaint();
				} else {
					// swipe down
					this.currentImage++;
					this.repaint();
				}
			}
		}
	},
	handleTouchEnd: function (e) {
		for ( var i = 0; i < e.changedTouches.length; i++ ) {
			var t = e.changedTouches;
			if ( !this.pendingTouches ) {
				continue;
			}
			var startX = this.pendingTouches;
			var startY = this.pendingTouches;
			var angle = Math.abs( Math.atan( ( startY - t.clientY ) / ( startX - t.clientX ) ) );
			if ( angle < 0.7 ) {
				// horizontal swipe. < 40 degrees
				if ( Math.abs( startX - t.clientX ) < 30 ) {
					// Not large enough
					continue;
				}

				if ( startX - t.clientX < 0 ) {
					// swipe right
					this.currentImage--;
					this.repaint();
				} else {
					// swipe left
					this.currentImage++;
					this.repaint();
				}
			}
			if ( angle > 1 ) {
				// vertical swipe. > ~60 degrees
				if ( Math.abs( startY - t.clientY ) < 30 ) {
					// Not large enough
					continue;
				}
				if ( startY - t.clientY > 0 ) {
					// swipe up
					this.currentImage--;
					this.repaint();
				} else {
					// swipe down
					this.currentImage++;
					this.repaint();
				}
			}

			delete this.pendingTouches;
		}
	},
};

// Include jquery.mousewheel dependency.
// --------
/*! Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh)
 * Licensed under the MIT License (LICENSE.txt).
 *
 * Version: 3.1.11
 *
 * Requires: jQuery 1.2.2+
 */

(function (factory) {
    if ( typeof define === 'function' && define.amd ) {
        // AMD. Register as an anonymous module.
        define(, factory);
    } else if (typeof exports === 'object') {
        // Node/CommonJS style for Browserify
        module.exports = factory;
    } else {
        // Browser globals
        factory(jQuery);
    }
}(function ($) {

    var toFix  = ,
        toBind = ( 'onwheel' in document || document.documentMode >= 9 ) ?
                     : ,
        slice  = Array.prototype.slice,
        nullLowestDeltaTimeout, lowestDelta;

    if ( $.event.fixHooks ) {
        for ( var i = toFix.length; i; ) {
            $.event.fixHooks ] = $.event.mouseHooks;
        }
    }

    var special = $.event.special.mousewheel = {
        version: '3.1.11',

        setup: function() {
            if ( this.addEventListener ) {
                for ( var i = toBind.length; i; ) {
                    this.addEventListener( toBind, handler, false );
                }
            } else {
                this.onmousewheel = handler;
            }
            // Store the line height and page height for this particular element
            $.data(this, 'mousewheel-line-height', special.getLineHeight(this));
            $.data(this, 'mousewheel-page-height', special.getPageHeight(this));
        },

        teardown: function() {
            if ( this.removeEventListener ) {
                for ( var i = toBind.length; i; ) {
                    this.removeEventListener( toBind, handler, false );
                }
            } else {
                this.onmousewheel = null;
            }
            // Clean up the data we added to the element
            $.removeData(this, 'mousewheel-line-height');
            $.removeData(this, 'mousewheel-page-height');
        },

        getLineHeight: function(elem) {
            var $parent = $(elem)();
            if (!$parent.length) {
                $parent = $('body');
            }
            return parseInt($parent.css('fontSize'), 10);
        },

        getPageHeight: function(elem) {
            return $(elem).height();
        },

        settings: {
            adjustOldDeltas: true, // see shouldAdjustOldDeltas() below
            normalizeOffset: true  // calls getBoundingClientRect for each event
        }
    };

    $.fn.extend({
        mousewheel: function(fn) {
            return fn ? this.on('mousewheel', fn) : this.trigger('mousewheel');
        },

        unmousewheel: function(fn) {
            return this.off('mousewheel', fn);
        }
    });


    function handler(event) {
        var orgEvent   = event || window.event,
            args       = slice.call(arguments, 1),
            delta      = 0,
            deltaX     = 0,
            deltaY     = 0,
            absDelta   = 0,
            offsetX    = 0,
            offsetY    = 0;
        event = $.event.fix(orgEvent);
        event.type = 'mousewheel';

        // Old school scrollwheel delta
        if ( 'detail'      in orgEvent ) { deltaY = orgEvent.detail * -1;      }
        if ( 'wheelDelta'  in orgEvent ) { deltaY = orgEvent.wheelDelta;       }
        if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY;      }
        if ( 'wheelDeltaX' in orgEvent ) { deltaX = orgEvent.wheelDeltaX * -1; }

        // Firefox < 17 horizontal scrolling related to DOMMouseScroll event
        if ( 'axis' in orgEvent && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) {
            deltaX = deltaY * -1;
            deltaY = 0;
        }

        // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy
        delta = deltaY === 0 ? deltaX : deltaY;

        // New school wheel delta (wheel event)
        if ( 'deltaY' in orgEvent ) {
            deltaY = orgEvent.deltaY * -1;
            delta  = deltaY;
        }
        if ( 'deltaX' in orgEvent ) {
            deltaX = orgEvent.deltaX;
            if ( deltaY === 0 ) { delta  = deltaX * -1; }
        }

        // No change actually happened, no reason to go any further
        if ( deltaY === 0 && deltaX === 0 ) { return; }

        // Need to convert lines and pages to pixels if we aren't already in pixels
        // There are three delta modes:
        //   * deltaMode 0 is by pixels, nothing to do
        //   * deltaMode 1 is by lines
        //   * deltaMode 2 is by pages
        if ( orgEvent.deltaMode === 1 ) {
            var lineHeight = $.data(this, 'mousewheel-line-height');
            delta  *= lineHeight;
            deltaY *= lineHeight;
            deltaX *= lineHeight;
        } else if ( orgEvent.deltaMode === 2 ) {
            var pageHeight = $.data(this, 'mousewheel-page-height');
            delta  *= pageHeight;
            deltaY *= pageHeight;
            deltaX *= pageHeight;
        }

        // Store lowest absolute delta to normalize the delta values
        absDelta = Math.max( Math.abs(deltaY), Math.abs(deltaX) );

        if ( !lowestDelta || absDelta < lowestDelta ) {
            lowestDelta = absDelta;

            // Adjust older deltas if necessary
            if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) {
                lowestDelta /= 40;
            }
        }

        // Adjust older deltas if necessary
        if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) {
            // Divide all the things by 40!
            delta  /= 40;
            deltaX /= 40;
            deltaY /= 40;
        }

        // Get a whole, normalized value for the deltas
        delta  = Math(delta  / lowestDelta);
        deltaX = Math(deltaX / lowestDelta);
        deltaY = Math(deltaY / lowestDelta);

        // Normalise offsetX and offsetY properties
        if ( special.settings.normalizeOffset && this.getBoundingClientRect ) {
            var boundingRect = this.getBoundingClientRect();
            offsetX = event.clientX - boundingRect.left;
            offsetY = event.clientY - boundingRect.top;
        }

        // Add information to the event object
        event.deltaX = deltaX;
        event.deltaY = deltaY;
        event.deltaFactor = lowestDelta;
        event.offsetX = offsetX;
        event.offsetY = offsetY;
        // Go ahead and set deltaMode to 0 since we converted to pixels
        // Although this is a little odd since we overwrite the deltaX/Y
        // properties with normalized deltas.
        event.deltaMode = 0;

        // Add event and delta to the front of the arguments
        args.unshift(event, delta, deltaX, deltaY);

        // Clearout lowestDelta after sometime to better
        // handle multiple device types that give different
        // a different lowestDelta
        // Ex: trackpad = 3 and mouse wheel = 120
        if (nullLowestDeltaTimeout) { clearTimeout(nullLowestDeltaTimeout); }
        nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200);

        return ($.event.dispatch || $.event.handle).apply(this, args);
    }

    function nullLowestDelta() {
        lowestDelta = null;
    }

    function shouldAdjustOldDeltas(orgEvent, absDelta) {
        // If this is an older event and the delta is divisable by 120,
        // then we are assuming that the browser is treating this as an
        // older mouse wheel event and that we should divide the deltas
        // by 40 to try and get a more usable deltaFactor.
        // Side note, this actually impacts the reported scroll distance
        // in older browsers and can cause scrolling to be slower than native.
        // Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false.
        return special.settings.adjustOldDeltas && orgEvent.type === 'mousewheel' && absDelta % 120 === 0;
    }

}));


// --- Start image stack popup
$( ImageStackPopup.init );