diff options
Diffstat (limited to 'www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboxinterface.js')
-rw-r--r-- | www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboxinterface.js | 509 |
1 files changed, 509 insertions, 0 deletions
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboxinterface.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboxinterface.js new file mode 100644 index 00000000..21f33a06 --- /dev/null +++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboxinterface.js @@ -0,0 +1,509 @@ +/* + * This file is part of the MediaWiki extension MultimediaViewer. + * + * MultimediaViewer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * MultimediaViewer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>. + */ + +( function ( mw, $, oo ) { + var LIP; + + /** + * Represents the main interface of the lightbox + * + * @class mw.mmv.LightboxInterface + * @extends mw.mmv.ui.Element + * @constructor + */ + function LightboxInterface() { + this.localStorage = mw.storage; + + /** @property {mw.mmv.Config} config - */ + this.config = new mw.mmv.Config( + mw.config.get( 'wgMultimediaViewer', {} ), + mw.config, + mw.user, + new mw.Api(), + this.localStorage + ); + + /** + * @property {mw.mmv.ThumbnailWidthCalculator} + * @private + */ + this.thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator(); + + this.init(); + mw.mmv.ui.Element.call( this, this.$wrapper ); + } + oo.inheritClass( LightboxInterface, mw.mmv.ui.Element ); + LIP = LightboxInterface.prototype; + + /** + * The currently selected LightboxImage. + * + * @type {mw.mmv.LightboxImage} + * @protected + */ + LIP.currentImage = null; + + /** + * Initialize the entire interface - helper method. + */ + LIP.init = function () { + // SVG filter, needed to achieve blur in Firefox + this.$filter = $( '<svg><filter id="gaussian-blur"><fegaussianblur stdDeviation="3"></filter></svg>' ); + + this.$wrapper = $( '<div>' ) + .addClass( 'mw-mmv-wrapper' ); + + this.$main = $( '<div>' ) + .addClass( 'mw-mmv-main' ); + + // I blame CSS for this + this.$innerWrapper = $( '<div>' ) + .addClass( 'mw-mmv-image-inner-wrapper' ); + + this.$imageWrapper = $( '<div>' ) + .addClass( 'mw-mmv-image-wrapper' ) + .append( this.$innerWrapper ); + + this.$preDiv = $( '<div>' ) + .addClass( 'mw-mmv-pre-image' ); + + this.$postDiv = $( '<div>' ) + .addClass( 'mw-mmv-post-image' ); + + this.$aboveFold = $( '<div>' ) + .addClass( 'mw-mmv-above-fold' ); + + this.$main.append( + this.$preDiv, + this.$imageWrapper, + this.$postDiv, + this.$filter + ); + + this.$wrapper.append( + this.$main + ); + + this.setupCanvasButtons(); + + this.panel = new mw.mmv.ui.MetadataPanel( this.$postDiv, this.$aboveFold, this.localStorage, this.config ); + this.buttons = new mw.mmv.ui.CanvasButtons( this.$preDiv, this.$closeButton, this.$fullscreenButton ); + this.canvas = new mw.mmv.ui.Canvas( this.$innerWrapper, this.$imageWrapper, this.$wrapper ); + + this.fileReuse = new mw.mmv.ui.reuse.Dialog( this.$innerWrapper, this.buttons.$reuse, this.config ); + this.downloadDialog = new mw.mmv.ui.download.Dialog( this.$innerWrapper, this.buttons.$download, this.config ); + this.optionsDialog = new mw.mmv.ui.OptionsDialog( this.$innerWrapper, this.buttons.$options, this.config ); + }; + + /** + * Sets up the file reuse data in the DOM + * + * @param {mw.mmv.model.Image} image + * @param {mw.mmv.model.Repo} repo + * @param {string} caption + * @param {string} alt + */ + LIP.setFileReuseData = function ( image, repo, caption, alt ) { + this.fileReuse.set( image, repo, caption, alt ); + this.downloadDialog.set( image, repo ); + }; + + /** + * Empties the interface. + */ + LIP.empty = function () { + this.panel.empty(); + + this.canvas.empty(); + + this.buttons.empty(); + + this.$main.addClass( 'metadata-panel-is-closed' ) + .removeClass( 'metadata-panel-is-open' ); + }; + + /** + * Opens the lightbox. + */ + LIP.open = function () { + this.empty(); + this.attach(); + }; + + /** + * Attaches the interface to the DOM. + * + * @param {string} [parentId] parent id where we want to attach the UI. Defaults to document + * element, override is mainly used for testing. + */ + LIP.attach = function ( parentId ) { + var ui = this, + $parent; + + // Advanced description needs to be below the fold when the lightbox opens + // regardless of what the scroll value was prior to opening the lightbox + // If the lightbox is already attached, it means we're doing prev/next, and + // we should avoid scrolling the panel + if ( !this.attached ) { + $( window ).scrollTop( 0 ); + } + + // Make sure that the metadata is going to be at the bottom when it appears + // 83 is the height of the top metadata area. Which can't be measured by + // reading the DOM at this point of the execution, unfortunately + this.$postDiv.css( 'top', ( $( window ).height() - 83 ) + 'px' ); + + // Re-appending the same content can have nasty side-effects + // Such as the browser leaving fullscreen mode if the fullscreened element is part of it + if ( this.currentlyAttached ) { + return; + } + + this.handleEvent( 'keyup', function ( e ) { + if ( e.keyCode === 27 && !( e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) ) { + // Escape button pressed + ui.unattach(); + } + } ); + + this.handleEvent( 'jq-fullscreen-change.lip', function ( e ) { + ui.fullscreenChange( e ); + } ); + + this.handleEvent( 'keydown', function ( e ) { ui.keydown( e ); } ); + + // mousemove generates a ton of events, which is why we throttle it + this.handleEvent( 'mousemove.lip', $.throttle( 250, function ( e ) { + ui.mousemove( e ); + } ) ); + + this.handleEvent( 'mmv-faded-out', function ( e ) { ui.fadedOut( e ); } ); + this.handleEvent( 'mmv-fade-stopped', function ( e ) { ui.fadeStopped( e ); } ); + + this.buttons.connect( this, { + next: [ 'emit', 'next' ], + prev: [ 'emit', 'prev' ] + } ); + + $parent = $( parentId || document.body ); + + // Clean up fullscreen data because hard-existing fullscreen might have left + // jquery.fullscreen unable to remove the class and attribute, since $main wasn't + // attached to the DOM anymore at the time the jq-fullscreen-change event triggered + this.$main.data( 'isFullscreened', false ).removeClass( 'jq-fullscreened' ); + this.isFullscreen = false; + + $parent + .append( + this.$wrapper + ); + this.currentlyAttached = true; + + this.panel.attach(); + + this.canvas.attach(); + + // cross-communication between panel and canvas, sort of + this.$postDiv.on( 'mmv-metadata-open.lip', function () { + ui.$main.addClass( 'metadata-panel-is-open' ) + .removeClass( 'metadata-panel-is-closed' ); + } ).on( 'mmv-metadata-close.lip', function () { + ui.$main.removeClass( 'metadata-panel-is-open' ) + .addClass( 'metadata-panel-is-closed' ); + } ); + this.$wrapper.on( 'mmv-panel-close-area-click.lip', function () { + ui.panel.scroller.toggle( 'down' ); + } ); + + // Buttons fading might not had been reset properly after a hard fullscreen exit + // This needs to happen after the parent attach() because the buttons need to be attached + // to the DOM for $.fn.stop() to work + this.buttons.stopFade(); + this.buttons.attach(); + + this.fileReuse.attach(); + this.downloadDialog.attach(); + this.optionsDialog.attach(); + + // Reset the cursor fading + this.fadeStopped(); + + this.attached = true; + }; + + /** + * Detaches the interface from the DOM. + */ + LIP.unattach = function () { + mw.mmv.actionLogger.log( 'close' ); + + // Has to happen first so that the scroller can freeze with visible elements + this.panel.unattach(); + + this.$wrapper.detach(); + + this.currentlyAttached = false; + + this.buttons.unattach(); + + this.$postDiv.off( '.lip' ); + this.$wrapper.off( 'mmv-panel-close-area-click.lip' ); + + this.fileReuse.unattach(); + this.fileReuse.closeDialog(); + + this.downloadDialog.unattach(); + this.downloadDialog.closeDialog(); + + this.optionsDialog.unattach(); + this.optionsDialog.closeDialog(); + + // Canvas listens for events from dialogs, so should be unattached at the end + this.canvas.unattach(); + + this.clearEvents(); + + this.buttons.disconnect( this, { + next: [ 'emit', 'next' ], + prev: [ 'emit', 'prev' ] + } ); + + // We trigger this event on the document because unattach() can run + // when the interface is unattached + $( document ).trigger( $.Event( 'mmv-close' ) ) + .off( 'jq-fullscreen-change.lip' ); + + this.attached = false; + }; + + /** + * Exits fullscreen mode. + */ + LIP.exitFullscreen = function () { + this.fullscreenButtonJustPressed = true; + this.$main.exitFullscreen(); + }; + + /** + * Enters fullscreen mode. + */ + LIP.enterFullscreen = function () { + this.$main.enterFullscreen(); + }; + + /** + * Setup for canvas navigation buttons + */ + LIP.setupCanvasButtons = function () { + var ui = this, + tooltipDelay = mw.config.get( 'wgMultimediaViewer' ).tooltipDelay; + + this.$closeButton = $( '<button>' ) + .text( ' ' ) + .addClass( 'mw-mmv-close' ) + .prop( 'title', mw.message( 'multimediaviewer-close-popup-text' ).text() ) + .tipsy( { + delayIn: tooltipDelay, + gravity: this.correctEW( 'ne' ) + } ) + .click( function () { + if ( ui.isFullscreen ) { + ui.$main.trigger( $.Event( 'jq-fullscreen-change.lip' ) ); + } + ui.unattach(); + } ); + + this.$fullscreenButton = $( '<button>' ) + .text( ' ' ) + .addClass( 'mw-mmv-fullscreen' ) + .prop( 'title', mw.message( 'multimediaviewer-fullscreen-popup-text' ).text() ) + .tipsy( { + delayIn: tooltipDelay, + gravity: this.correctEW( 'ne' ) + } ) + .click( function ( e ) { + if ( ui.isFullscreen ) { + ui.exitFullscreen(); + + // mousemove is throttled and the mouse coordinates only + // register every 250ms, so there is a chance that we moved + // our mouse over one of the buttons but it didn't register, + // and a fadeOut is triggered; when we're coming back from + // fullscreen, we'll want to make sure the mouse data is + // current so that the fadeOut behavior will not trigger + ui.mousePosition = { x: e.pageX, y: e.pageY }; + ui.buttons.revealAndFade( ui.mousePosition ); + } else { + ui.enterFullscreen(); + } + } ); + + // If the browser doesn't support fullscreen mode, hide the fullscreen button + if ( $.support.fullscreen ) { + this.$fullscreenButton.show(); + } else { + this.$fullscreenButton.hide(); + } + }; + + /** + * Handle a fullscreen change event. + * + * @param {jQuery.Event} e The fullscreen change event. + */ + LIP.fullscreenChange = function ( e ) { + this.isFullscreen = e.fullscreen; + + if ( this.isFullscreen ) { + mw.mmv.actionLogger.log( 'fullscreen' ); + + this.$fullscreenButton + .prop( 'title', mw.message( 'multimediaviewer-defullscreen-popup-text' ).text() ) + .attr( 'alt', mw.message( 'multimediaviewer-defullscreen-popup-text' ).text() ); + } else { + mw.mmv.actionLogger.log( 'defullscreen' ); + + this.$fullscreenButton + .prop( 'title', mw.message( 'multimediaviewer-fullscreen-popup-text' ).text() ) + .attr( 'alt', mw.message( 'multimediaviewer-fullscreen-popup-text' ).text() ); + } + + if ( !this.fullscreenButtonJustPressed && !e.fullscreen ) { + // Close the interface all the way if the user pressed 'esc' + this.unattach(); + } else if ( this.fullscreenButtonJustPressed ) { + this.fullscreenButtonJustPressed = false; + } + + // Fullscreen change events can happen after unattach(), in which + // case we shouldn't do anything UI-related + if ( !this.currentlyAttached ) { + return; + } + + if ( this.isFullscreen ) { + // When entering fullscreen without a mousemove, the browser + // still thinks that the cursor is where it was prior to entering + // fullscreen. I.e. on top of the fullscreen button + // Thus, we purposefully reset the saved position, so that + // the fade out really takes place (otherwise it's cancelled + // by updateControls which is called a few times when fullscreen opens) + this.mousePosition = { x: 0, y: 0 }; + this.buttons.fadeOut(); + } + + // Some browsers only send resize events before toggling fullscreen, but not once the toggling is done + // This makes sure that the UI is properly resized after a fullscreen change + this.$main.trigger( $.Event( 'mmv-resize-end' ) ); + }; + + /** + * Handles keydown events on the document + * + * @param {jQuery.Event} e The jQuery keypress event object + */ + LIP.keydown = function ( e ) { + var forward, + isRtl = $( document.body ).hasClass( 'rtl' ); + + if ( e.altKey || e.shiftKey || e.ctrlKey || e.metaKey ) { + return; + } + + switch ( e.which ) { + case 37: // Left arrow + case 39: // Right arrow + e.preventDefault(); + forward = ( e.which === 39 ); + if ( isRtl ) { + forward = !forward; + } + + if ( forward ) { + this.emit( 'next' ); + } else { + this.emit( 'prev' ); + } + + e.preventDefault(); + break; + } + }; + + /** + * Handles mousemove events on the document + * + * @param {jQuery.Event} e The mousemove event object + */ + LIP.mousemove = function ( e ) { + // T77869 ignore fake mousemove events triggered by Chrome + if ( + e && + e.originalEvent && + e.originalEvent.movementX === 0 && + e.originalEvent.movementY === 0 + ) { + return; + } + + if ( e ) { + // Saving the mouse position is useful whenever we need to + // run LIP.mousemove manually, such as when going to the next/prev + // element + this.mousePosition = { x: e.pageX, y: e.pageY }; + } + + if ( this.isFullscreen ) { + this.buttons.revealAndFade( this.mousePosition ); + } + }; + + /** + * Called when the buttons have completely faded out and disappeared + */ + LIP.fadedOut = function () { + this.$main.addClass( 'cursor-hidden' ); + }; + + /** + * Called when the buttons have stopped fading and are back into view + */ + LIP.fadeStopped = function () { + this.$main.removeClass( 'cursor-hidden' ); + }; + + /** + * Updates the next and prev buttons + * + * @param {boolean} showPrevButton Whether the prev button should be revealed or not + * @param {boolean} showNextButton Whether the next button should be revealed or not + */ + LIP.updateControls = function ( showPrevButton, showNextButton ) { + var prevNextTop = ( ( this.$imageWrapper.height() / 2 ) - 60 ) + 'px'; + + if ( this.$main.data( 'isFullscreened' ) ) { + this.$postDiv.css( 'top', '' ); + } else { + this.$postDiv.css( 'top', this.$imageWrapper.height() ); + } + + this.buttons.setOffset( prevNextTop ); + this.buttons.toggle( showPrevButton, showNextButton ); + }; + + mw.mmv.LightboxInterface = LightboxInterface; +}( mediaWiki, jQuery, OO ) ); |