User:SDZeroBot/sync/MediaWiki:Gadget-ImageStackPopup.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:SDZeroBot/sync/MediaWiki:Gadget-ImageStackPopup. This user script seems to have an accompanying .css page at User:SDZeroBot/sync/MediaWiki:Gadget-ImageStackPopup.css. |
/******************************************************************************/
/**** THIS PAGE TRACKS [[mw:MediaWiki:Gadget-Global-ImageStackPopup.js]]. PLEASE AVOID EDITING DIRECTLY.
/**** EDITS SHOULD BE PROPOSED DIRECTLY to [[mw:MediaWiki:Gadget-Global-ImageStackPopup.js]].
/**** 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[ userLanguage ] );
return;
}
var pageLanguage = mw.config.get( 'wgPageContentLanguage' );
if ( pageLanguage in ImageStackPopup.messages ) {
mw.messages.set( ImageStackPopup.messages[ pageLanguage ] );
return;
}
var contentLanguage = mw.config.get( 'wgContentLanguage' );
if ( contentLanguage in ImageStackPopup.messages ) {
mw.messages.set( ImageStackPopup.messages[ contentLanguage ] );
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[i] instanceof Object && typeof viewerInfo[i].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[i];
$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: [ 'primary', 'progressive' ]
} ],
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( [ new dialog() ] );
// 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.org/wiki/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[0].width;
var height = imgs[0].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[i].match( /^(\S+)\s+([0-9.])x\s*$/ );
if (
parts &&
parts.length === 3
) {
var pixelRatio = parseFloat( parts[2] );
if (
( imgW < w && originalW*pixelRatio > imgW ) ||
( imgW > w && originalW*pixelRatio - w >= 0 && originalW*pixelRatio < imgW )
) {
imgW = originalW*pixelRatio;
imgH = originalH*pixelRatio;
src = parts[1];
}
}
}
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( /[^a-zA-Z0-9_]/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[0].height)/(imgs[0].width);
}
if ( !this.width && this.height ) {
this.width = this.height * (imgs[0].width)/(imgs[0].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.org/wiki/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[0].parentElement.parentElement.parentElement.clientWidth;
var containingHeight = this.$viewer[0].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[0].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[0].clientHeight;
var paddingDivStyles = getComputedStyle( this.$viewer[0].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[0].clientHeight;
}
controlHeight += 5; // fudge factor.
if ( this.$viewer[0].parentElement.previousElementSibling ) {
// OOUI window label. This is a bit hacky.
controlHeight += this.$viewer[0].parentElement.previousElementSibling.clientHeight;
}
var maxImgDim = this.getMaxImgDim();
var aspect = maxImgDim[0]/maxImgDim[1];
containingHeight -= controlHeight;
// 3 to account for slider and text controls. but not on narrow screens.
if ( containingWidth >= 500 ) {
containingWidth -= parseFloat( getComputedStyle( this.$slider[0] ).getPropertyValue( 'width' ) ) * 3;
}
if ( maxImgDim[0] > maxImgDim[1] ) {
if ( maxImgDim[0] > containingWidth ) {
// shrink to fit.
maxImgDim[0] = containingWidth;
maxImgDim[1] = Math.floor(containingWidth/aspect);
}
if ( maxImgDim[1] > containingHeight ) {
maxImgDim[1] = containingHeight;
maxImgDim[0] = Math.floor( containingHeight * aspect );
}
} else {
if ( maxImgDim[1] > containingHeight ) {
maxImgDim[1] = containingHeight;
maxImgDim[0] = Math.floor( containingHeight * aspect );
}
if ( maxImgDim[0] > containingWidth ) {
// shrink to fit.
maxImgDim[0] = containingWidth;
maxImgDim[1] = Math.floor(containingWidth/aspect);
}
}
this.width = maxImgDim[0];
this.height = maxImgDim[1];
}
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[0] ).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[0].width;
var h = this.imgs[0].height;
if ( this.imgs[0].srcset.match( /\s2x\s*(,|$)/ ) ) {
w *= 2;
h *= 2;
} else if ( this.imgs[0].srcset.match( /\s1.5x\s*(,|$)/ ) ) {
w = Math.floor( 1.5*w );
h = Math.floor( 1.5*h );
}
return [w,h];
},
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[0].value = this.currentImage;
// Future todo might be to localize digits.
this.$currentCount[0].textContent = this.currentImage + 1;
this.img.src = this.urls[this.currentImage];
this.$credit[0].href = this.infoUrls[this.currentImage];
if ( this.infoUrls[this.currentImage] === 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[i];
}
},
getUrls: function () {
this.urls = [];
this.infoUrls = [];
for( var i = 0; i < this.imgs.length; i++ ) {
this.urls[i] = ImageStackPopup.getSource( this.imgs[i], this.width, this.height );
if ( this.imgs[i].parentElement.href ) {
this.infoUrls[i] = this.imgs[i].parentElement.href;
} else {
this.infoUrls[i] = 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[i];
this.pendingTouches[t.identifier] = [t.clientX, t.clientY];
}
},
handleTouchCancel: function (e) {
for ( var i = 0; i < e.changedTouches.length; i++ ) {
var t = e.changedTouches[i];
delete this.pendingTouches[t.identifier];
}
},
handleTouchMove: function (e) {
for ( var i = 0; i < e.changedTouches.length; i++ ) {
var t = e.changedTouches[i];
if ( !this.pendingTouches[t.identifier] ) {
continue;
}
var startX = this.pendingTouches[t.identifier][0];
var startY = this.pendingTouches[t.identifier][1];
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[t.identifier] = [t.clientX, t.clientY];
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[i];
if ( !this.pendingTouches[t.identifier] ) {
continue;
}
var startX = this.pendingTouches[t.identifier][0];
var startY = this.pendingTouches[t.identifier][1];
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[t.identifier];
}
},
};
// 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(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node/CommonJS style for Browserify
module.exports = factory;
} else {
// Browser globals
factory(jQuery);
}
}(function ($) {
var toFix = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'],
toBind = ( 'onwheel' in document || document.documentMode >= 9 ) ?
['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'],
slice = Array.prototype.slice,
nullLowestDeltaTimeout, lowestDelta;
if ( $.event.fixHooks ) {
for ( var i = toFix.length; i; ) {
$.event.fixHooks[ toFix[--i] ] = $.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[--i], 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[--i], 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)['offsetParent' in $.fn ? 'offsetParent' : 'parent']();
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 >= 1 ? 'floor' : 'ceil' ](delta / lowestDelta);
deltaX = Math[ deltaX >= 1 ? 'floor' : 'ceil' ](deltaX / lowestDelta);
deltaY = Math[ deltaY >= 1 ? 'floor' : 'ceil' ](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 );