மீடியாவிக்கி:Gadget-Global-ImageStackPopup.js
Jump to navigation
Jump to search
குறிப்பு - சேமித்த பின்னர், நீங்கள் செய்த மாற்றங்களைக் காண்பதற்கு உங்கள் உலவியின் இடைமாற்று அகற்றப்பட வேண்டும்.
- மொஸில்லா பயர்பாக்ஸ் / சபாரி: Shift+Reload, அல்லது Ctrl-F5 அல்லது Ctrl-R (⌘-R Mac ல்)
- கூகிள் குரோம் Ctrl-Shift-R அழுத்தவும். (⌘-Shift-R Mac ல்) ;
- இண்டர்நெட் எக்ஸ்ப்ளோரர்: Ctrl-Refresh அல்லது Ctrl-F5 ஐ அழுத்தவும்.
- ஒபேரா: Tools → Preferences இல் இடைமாற்றை அகற்றவும்;
// 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; }, Context: function ( $viewer, config, imgs, width, height ) { 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 );