/* * 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 . */ ( function ( mw, $, moment ) { var MultiLightbox, lightboxHooks, MMVP, comingFromPopstate = false, imgsSelector = '.gallery .image img, a.image img', validExtensions = { 'jpg': true, 'jpeg': true, 'gif': true, 'svg': true, 'png': true, 'tiff': true, 'tif': true }, iiprops = [ 'timestamp', 'user', 'userid', 'comment', 'url', 'size', 'sha1', 'mime', 'mediatype', 'metadata', 'extmetadata' ], mmvLogActions = { 'thumbnail-link-click': 'User clicked on thumbnail to open lightbox.', 'enlarge-link-click': 'User clicked on enlarge link to open lightbox.', 'fullscreen-link-click': 'User clicked on fullscreen button in lightbox.', 'defullscreen-link-click': 'User clicked on button to return to normal lightbox view.', 'close-link-click': 'User clicked on the lightbox close button.', 'site-link-click': 'User clicked on the link to the file description page.', // Profiling events start messages, $1 replaced with profile ID 'profile-image-load-start': 'Profiling image load with ID $1', 'profile-image-resize-start': 'Profiling image resize with ID $1', 'profile-metadata-fetch-start': 'Profiling image metadata fetch with ID $1', 'profile-gender-fetch-start': 'Profiling uploader gender fetch with ID $1', // Profiling events end messages, $1 replaced with profile ID, $2 replaced with time it took in ms 'profile-image-load-end': 'Finished image load with ID $1 in $2 milliseconds', 'profile-image-resize-end': 'Finished image resize with ID $1 in $2 milliseconds', 'profile-metadata-fetch-end': 'Finished image metadata fetch with ID $1 in $2 milliseconds', 'profile-gender-fetch-end': 'Finished uploader gender fetch with ID $1 in $2 milliseconds' }; /** * @class mw.MultimediaViewer * Analyses the page, looks for image content and sets up the hooks * to manage the viewing experience of such content. * @constructor */ function MultimediaViewer() { /** * MultiLightbox object used to display the pictures in the page. * @property {mlb.MultiLightbox} * @private */ this.lightbox = null; /** * Whether we've fired an animation for the metadata div. * @property {boolean} * @private */ this.hasAnimatedMetadata = false; var $thumbs = $( imgsSelector ), urls = [], viewer = this; /** * @property {number[]} * @private * List of acceptable image sizes...used to bucket */ this.imageWidthBuckets = [ 320, 640, 800, 1024, 1280, 1920, 2560, 2880 ]; /** * @property {mw.Api} * @private */ this.api = new mw.Api(); /** * @property {mw.mmv.provider.ImageUsage} * @private */ this.imageUsageDataProvider = new mw.mmv.provider.ImageUsage( this.api ); /** * @property {mw.mmv.provider.GlobalUsage} * @private */ this.globalUsageDataProvider = new mw.mmv.provider.GlobalUsage( this.api, { doNotUseApi: !mw.config.get( 'wgMultimediaViewer' ).globalUsageAvailable } ); // replace with this one to test global usage on a local wiki without going through all the // hassle required for installing the extension: //this.globalUsageDataProvider = new mw.mmv.provider.GlobalUsage( // new mw.Api( {ajax: { url: 'http://commons.wikimedia.org/w/api.php', dataType: 'jsonp' } } ) //); /** * imageInfo object, used for caching - promises will resolve with * an mw.mmv.model.Image object, a repoInfo object, the best width for * the current screen configuration, and the width requested from * the server (if any). * @property {jQuery.Promise[]} * @private */ this.imageInfo = {}; // Traverse DOM, looking for potential thumbnails $thumbs.each( function ( i, thumb ) { var thisImage, $thumbCaption, caption, $thumb = $( thumb ), $link = $thumb.closest( 'a.image' ), $thumbContain = $link.closest( '.thumb' ), $enlarge = $thumbContain.find( '.magnify a' ), $links = $link.add( $enlarge ), filePageLink = $link.prop( 'href' ), fileTitle = mw.Title.newFromImg( $thumb ), index = urls.length; if ( !validExtensions[fileTitle.getExtension().toLowerCase()] ) { // Not a valid extension, skip this one return; } if ( $thumbContain.length === 0 ) { // This isn't a thumbnail! Just use the link. $thumbContain = $link; } else if ( $thumbContain.is( '.thumb' ) ) { $thumbCaption = $thumbContain.find( '.thumbcaption' ).clone(); $thumbCaption.find( '.magnify' ).remove(); viewer.whitelistHtml( $thumbCaption ); caption = $thumbCaption.html(); $thumbContain = $thumbContain.find( '.image' ); } $links.data( 'filePageLink', filePageLink ); // Create a LightboxImage object for each legit image thisImage = viewer.createNewImage( $thumb.prop( 'src' ), filePageLink, fileTitle, index, thumb, caption ); urls.push( thisImage ); // Register callback that launches modal image viewer if valid click $links.click( function ( e ) { return viewer.clickLinkCallback( e, this, $thumbContain, thisImage ); } ); } ); if ( urls.length === 0 ) { // No legit images found, no need to continue return; } // Only if we find legit images, create a MultiLightbox object this.lightbox = new mw.MultiLightbox( urls, 0, mw.LightboxInterface, this ); // Register various event hooks. TODO: Make this a function that's only called once. lightboxHooks.register( 'closeInterface', function () { if ( this.$nextButton ) { this.$nextButton.add( this.$prevButton ).css( 'top', '-999px' ); } $( document.body ).removeClass( 'mw-mlb-lightbox-open' ); if ( comingFromPopstate === false ) { history.pushState( {}, '', '#' ); } else { comingFromPopstate = false; } viewer.hasAnimatedMetadata = false; viewer.isOpen = false; } ); lightboxHooks.register( 'imageResize', function () { var ui = this; viewer.resize( ui ); return false; } ); } MMVP = MultimediaViewer.prototype; // TODO FIXME HACK delete this when other UI elements have been shifted away. MMVP.whitelistHtml = mw.mmv.ui.Element.prototype.whitelistHtml; /** * Create an image object for the lightbox to use. * @protected * @param {string} fileLink Link to the file - generally a thumb URL * @param {string} filePageLink Link to the File: page * @param {mw.Title} fileTitle Represents the File: page * @param {number} index Which number file this is * @param {HTMLImageElement} thumb The thumbnail that represents this image on the page * @param {string} [caption] The caption, if any. * @returns {mw.LightboxImage} */ MMVP.createNewImage = function ( fileLink, filePageLink, fileTitle, index, thumb, caption ) { var thisImage = new mw.LightboxImage( fileLink, filePageLink, fileTitle, index, thumb, caption ); thisImage.filePageLink = filePageLink; thisImage.filePageTitle = fileTitle; thisImage.index = index; thisImage.thumbnail = thumb; return thisImage; }; /** * Finds the next highest image size given a target size. * Searches the bucketed sizes configured in the class. * @param {number} target * @return {number} */ MMVP.findNextHighestImageSize = function ( target ) { var i, bucket, buckets = this.imageWidthBuckets, len = buckets.length; for ( i = 0; i < len; i++ ) { bucket = buckets[i]; if ( bucket >= target ) { return bucket; } } // If we failed to find a high enough size...good luck return bucket; }; /** * Gets the API arguments for various calls to the API to find sized thumbnails. * @param {mw.LightboxInterface} ui * @returns {Object} * @returns {number} return.requested The width that should be requested from the API * @returns {number} return.target The ideal width we would like to have - should be the width of the image element later. */ MMVP.getImageSizeApiArgs = function ( ui ) { var requestedWidth, calculatedMaxWidth, thumb = ui.currentImage.thumbnail, targetWidth = ui.$imageWrapper.width(), targetHeight = ui.$imageWrapper.height(); if ( ( targetWidth / targetHeight ) > ( thumb.width / thumb.height ) ) { // Need to find width corresponding to highest height we can have. calculatedMaxWidth = ( thumb.width / thumb.height ) * targetHeight; requestedWidth = this.findNextHighestImageSize( calculatedMaxWidth ); } else { // Simple case, ratio tells us we're limited by width requestedWidth = this.findNextHighestImageSize( targetWidth ); } return { // Factor in pixel ratio so we get as many pixels as the device supports, see b/60388 requested: requestedWidth * $.devicePixelRatio(), target: calculatedMaxWidth || targetWidth }; }; /** * Handles clicks on legit image links. * * @protected * * @param {jQuery.Event} e click event * @param {HTMLElement|jQuery} clickedEle clicked element * @param {jQuery} $thumbContain thumbnail container element * @param {mw.LightboxImage} thisImage lightboximage object */ MMVP.clickLinkCallback = function ( e, clickedEle, $thumbContain, thisImage ) { // Do not interfere with non-left clicks or if modifier keys are pressed. if ( e.which !== 1 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) { return; } var $clickedEle = $( clickedEle ), initial = $thumbContain.find( 'img' ).prop( 'src' ); if ( $clickedEle.is( 'a.image' ) ) { this.log( 'thumbnail-link-click' ); } else if ( $clickedEle.is( '.magnify a' ) ) { this.log( 'enlarge-link-click' ); } e.preventDefault(); this.loadImage( thisImage, initial ); return false; }; /** * Handles resize events in viewer. * * @protected * * @param {mw.LightboxInterface} ui lightbox that got resized */ MMVP.resize = function ( ui ) { var viewer = this, fileTitle = this.currentImageFileTitle; if ( fileTitle ) { this.fetchImageInfo( fileTitle, [ 'url' ] ).done( function ( imageData, repoInfo, targetWidth, requestedWidth ) { viewer.loadResizedImage( ui, imageData, targetWidth, requestedWidth ); } ); } this.updateControls(); }; /** * Replaces the resized image in the viewer providing we actually got some data. * * @protected * * @param {mw.LightboxInterface} ui lightbox that got resized * @param {mw.mmv.model.Image} imageData information regarding the new resized image * @param {number} targetWidth * @param {number} requestedWidth */ MMVP.loadResizedImage = function ( ui, imageData, targetWidth, requestedWidth ) { // Replace image only if data was returned. if ( imageData ) { this.loadAndSetImage( ui, imageData, targetWidth, requestedWidth, 'image-resize' ); } }; MMVP.updateControls = function () { var ui = this.ui, prevNextTop = ( ( ui.$imageWrapper.height() / 2 ) - 60 ) + 'px'; ui.$postDiv.css( 'top', ui.$imageWrapper.height() ); ui.$nextButton.add( ui.$prevButton ).css( { top: prevNextTop } ); ui.$nextButton.toggleClass( 'disabled', this.lightbox.currentIndex >= ( ( this.lightbox.images ? this.lightbox.images.length : 0 ) - 1 ) ); ui.$prevButton.toggleClass( 'disabled', this.lightbox.currentIndex <= 0 ); }; MMVP.registerLogging = function () { var viewer = this; this.ui.$closeButton.click( function () { if ( viewer.ui.$dialog ) { viewer.ui.$dialog.dialog( 'close' ); } viewer.log( 'close-link-click' ); } ); this.ui.$fullscreenButton.click( function () { if ( viewer.ui.isFullscreen ) { viewer.log( 'fullscreen-link-click' ); } else { viewer.log( 'defullscreen-link-click' ); } } ); }; MMVP.cacheRepoInfo = function ( repos ) { var i, repo; repos = repos || []; if ( this.repoInfo === undefined ) { this.repoInfo = {}; } for ( i = 0; i < repos.length; i++ ) { repo = repos[i]; this.repoInfo[repo.name] = repo; } }; /** * @method * Get first (hopefully only) member of an object. * @param {Array|Object} things * @returns {Mixed} */ MMVP.getFirst = function ( things ) { var thing; if ( things ) { $.each( things, function ( i, thisone ) { thing = thisone; return false; } ); } return thing; }; /** * @method * Set the image information in the UI. * @param {mw.LightboxImage} image * @param {mw.mmv.model.Image} imageData * @param {mw.mmv.model.Repo} repoData * @param {mw.mmv.model.FileUsage} localUsage * @param {mw.mmv.model.FileUsage} globalUsage */ MMVP.setImageInfo = function ( image, imageData, repoData, localUsage, globalUsage ) { var gfpid, msgname, fileTitle = image.filePageTitle, caption = image.caption, viewer = this, ui = this.lightbox.iface; ui.$title.text( fileTitle.getNameText() ); ui.initUseFileData( fileTitle, imageData.url, repoData.isLocal ); ui.$useFileLi.removeClass( 'empty' ); ui.setRepoDisplay( repoData.displayName, repoData.favIcon, repoData.isLocal ); ui.setFilePageLink( imageData.descriptionUrl ); ui.$repoLi.removeClass( 'empty' ); if ( imageData.lastUploader ) { gfpid = this.profileStart( 'gender-fetch' ); // TODO: Reuse the api member, fix everywhere. // Fetch the gender from the uploader's home wiki // TODO this is ugly as hell, let's fix this in core. new mw.Api( { ajax: { url: repoData.apiUrl || mw.util.wikiScript( 'api' ), dataType: 'jsonp' } } ).get( { action: 'query', list: 'users', ususers: imageData.lastUploader, usprop: 'gender' } ).done( function ( data ) { var gender = 'unknown'; viewer.profileEnd( gfpid ); if ( data && data.query && data.query.users && data.query.users[0] && data.query.users[0].gender ) { gender = data.query.users[0].gender; } ui.setUserPageLink( repoData, imageData.lastUploader, gender ); } ).fail( function () { mw.log( 'Gender fetch with ID ' + gfpid + ' failed, probably due to cross-domain API request.' ); ui.setUserPageLink( repoData, imageData.lastUploader, 'unknown' ); } ); } if ( imageData.creationDateTime ) { ui.$datetime.text( mw.message( 'multimediaviewer-datetime-created', this.formatDate( imageData.creationDateTime ) ).text() ); } else if ( imageData.uploadDateTime ) { ui.$datetime.text( mw.message( 'multimediaviewer-datetime-uploaded', this.formatDate( imageData.uploadDateTime ) ).text() ); } ui.$datetimeLi.toggleClass( 'empty', !imageData.uploadDateTime && !imageData.creationDateTime ); if ( imageData.source ) { this.whitelistHtml( ui.$source.empty().append( $.parseHTML( imageData.source ) ) ); } if ( imageData.author ) { this.whitelistHtml( ui.$author.empty().append( $.parseHTML( imageData.author ) ) ); } if ( imageData.source && imageData.author ) { ui.$credit.html( mw.message( 'multimediaviewer-credit', ui.$author.get( 0 ).outerHTML, ui.$source.get( 0 ).outerHTML ).plain() ); } else { // Clobber the contents and only have one of the fields if ( imageData.source ) { ui.$credit.empty().append( ui.$source ); } else if ( imageData.author ) { ui.$credit.empty().append( ui.$author ); } } ui.$credit.toggleClass( 'empty', !imageData.source && !imageData.author ); ui.description.set( imageData.description, caption ); msgname = 'multimediaviewer-license-' + ( imageData.license || '' ); if ( !imageData.license || !mw.messages.exists( msgname ) ) { // Cannot display, fallback or fail msgname = 'multimediaviewer-license-default'; } else { // License found, store the license data ui.$license.data( 'license', mw.message( msgname ).text() ); } ui.$license .text( mw.message( msgname ).text() ) .toggleClass( 'cc-license', imageData.isCcLicensed() ); ui.$license.toggleClass( 'empty', !imageData.license ); this.setLocationData( imageData ); ui.$locationLi.toggleClass( 'empty', !imageData.hasCoords() ); ui.fileUsage.set( localUsage, globalUsage ); }; /** * @method * Sets location data in the interface. * @param {mw.mmv.model.Image} imageData */ MMVP.setLocationData = function ( imageData ) { var latsec, latitude, latmsg, latdeg, latremain, latmin, longsec, longitude, longmsg, longdeg, longremain, longmin; if ( !imageData.hasCoords() ) { return; } latitude = imageData.latitude >= 0 ? imageData.latitude : imageData.latitude * -1; latmsg = 'multimediaviewer-geoloc-' + ( imageData.latitude >= 0 ? 'north' : 'south' ); latdeg = Math.floor( latitude ); latremain = latitude - latdeg; latmin = Math.floor( ( latremain ) * 60 ); longitude = imageData.longitude >= 0 ? imageData.longitude : imageData.longitude * -1; longmsg = 'multimediaviewer-geoloc-' + ( imageData.longitude >= 0 ? 'east' : 'west' ); longdeg = Math.floor( longitude ); longremain = longitude - longdeg; longmin = Math.floor( ( longremain ) * 60 ); longremain -= longmin / 60; latremain -= latmin / 60; latsec = Math.round( latremain * 100 * 60 * 60 ) / 100; longsec = Math.round( longremain * 100 * 60 * 60 ) / 100; this.lightbox.iface.setLocationData( latdeg, latmin, latsec, latmsg, longdeg, longmin, longsec, longmsg, imageData.latitude, imageData.longitude, this.getFirst( Object.keys( mw.language.data ) ), imageData.title.getMain() ); }; /** * @method * Loads and sets the image specified in the imageData. It also updates the controls * and collects profiling information. * * @param {mw.LightboxInterface} ui image container * @param {mw.mmv.model.Image} imageData image information * @param {number} targetWidth * @param {number} requestedWidth * @param {string} profileEvent profile event key */ MMVP.loadAndSetImage = function ( ui, imageData, targetWidth, requestedWidth, profileEvent ) { var rpid, maybeThumb, viewer = this, image = new Image(); image.onload = function () { viewer.profileEnd( rpid ); }; rpid = this.profileStart( profileEvent, { width: imageData.width, height: imageData.height, fileSize: imageData.size }, imageData.mimeType ); // Use cached image if we have it. maybeThumb = imageData.getThumbUrl( requestedWidth ); image.src = maybeThumb || imageData.url; if ( maybeThumb && requestedWidth > targetWidth || !maybeThumb && imageData.width > targetWidth ) { // Image bigger than the current area, resize before loading image.width = targetWidth; } ui.replaceImageWith( image ); this.updateControls(); }; /** * @method * Loads a specified image. * @param {mw.LightboxImage} image * @param {string} initialSrc The string to set the src attribute to at first. */ MMVP.loadImage = function ( image, initialSrc ) { var mdpid, viewer = this; this.lightbox.currentIndex = image.index; // Open with the already-loaded thumbnail // Avoids trying to load /wiki/Undefined and doesn't // cost any network time - the library currently needs // some src attribute to work. Will fix. image.initialSrc = initialSrc; this.currentImageFilename = image.filePageTitle.getPrefixedText(); this.currentImageFileTitle = image.filePageTitle; this.lightbox.iface.comingFromPopstate = comingFromPopstate; if ( !this.isOpen ) { this.lightbox.open(); this.isOpen = true; } else { this.lightbox.iface.empty(); this.lightbox.iface.load( image ); } $( document.body ).addClass( 'mw-mlb-lightbox-open' ); mdpid = this.profileStart( 'metadata-fetch' ); this.fetchImageInfoAndFileUsageInfo( image.filePageTitle ).then( function ( imageData, repoInfo, targetWidth, requestedWidth, localUsage, globalUsage ) { var repoData = mw.mmv.model.Repo.newFromRepoInfo( repoInfo[imageData.repo] ); viewer.profileEnd( mdpid ); viewer.stopListeningToScroll(); viewer.animateMetadataDivOnce() // We need to wait until the animation is finished before we listen to scroll .then( function() { viewer.startListeningToScroll(); } ); viewer.loadAndSetImage( viewer.lightbox.iface, imageData, targetWidth, requestedWidth, 'image-load' ); viewer.lightbox.iface.$imageDiv.removeClass( 'empty' ); viewer.setImageInfo( image, imageData, repoData, localUsage, globalUsage ); } ); comingFromPopstate = false; }; /** * @method * Animates the metadata area when the viewer is first opened. * @return {jQuery.Promise} an empty promise which resolves when the animation is finished */ MMVP.animateMetadataDivOnce = function () { if ( !this.hasAnimatedMetadata ) { this.hasAnimatedMetadata = true; $.scrollTo( 40, 400 ) .scrollTo( 0, 400 ); } return $.scrollTo.window().promise(); }; /** * @method * Stop listening to the page's scroll events */ MMVP.stopListeningToScroll = function () { $.scrollTo().off( 'scroll.mmvp' ); }; /** * @method * Start listening to the page's scroll events * Will call MMVP.scroll(), throttled so it is not triggered on every pixel. */ MMVP.startListeningToScroll = function () { var viewer = this; $.scrollTo().on( 'scroll.mmvp', $.throttle( 250, function() { viewer.scroll(); } ) ); // Trigger a check in case the user scrolled manually during the animation viewer.scroll(); }; /** * @method * Receives the window's scroll events and flips the chevron if necessary. */ MMVP.scroll = function () { this.ui.$dragIcon.toggleClass( 'pointing-down', !!$.scrollTo().scrollTop() ); }; /** * @method * Fetches image information from the API. * * Will resolve the promise with two objects (imageData and repoData), the * target width - basically the screen size - that the caller should resize * the image to eventually, and the requested width - that is, what we asked * for from the API - that should be used to fetch the thumbnail URL from * the imageData object. * * The target * @param {mw.Title} fileTitle Title of the file page for the image. * @param {string[]} [props] List of properties to get from imageinfo * @returns {jQuery.Promise} */ MMVP.fetchImageInfo = function ( fileTitle, props ) { var filename = fileTitle.getPrefixedText(), apiArgs = { action: 'query', format: 'json', titles: filename, prop: 'imageinfo', // Short-circuit, don't fallback, to save some tiny amount of time iiextmetadatalanguage: mw.config.get( 'wgUserLanguage', false ) || mw.config.get( 'wgContentLanguage', 'en' ) }, viewer = this, widths = this.getImageSizeApiArgs( this.ui ), targetWidth = widths.target, requestedWidth = widths.requested; function handleApiData( data ) { var imageInfo, imageData; if ( !data || !data.query ) { // No information, oh well return $.Deferred().reject(); } viewer.cacheRepoInfo( data.query.repos ); imageInfo = viewer.getFirst( data.query.pages ); if ( imageInfo ) { imageData = mw.mmv.model.Image.newFromImageInfo( fileTitle, imageInfo ); // Give back the information we have return $.Deferred().resolve( imageData, viewer.repoInfo, targetWidth, requestedWidth ); } else { return $.Deferred().reject(); } } function makeImageInfoRequest( args ) { if ( viewer.repoInfo === undefined ) { args.meta = 'filerepoinfo'; } return viewer.api.get( args ).then( handleApiData ); } props = props || iiprops; apiArgs.iiprop = props.join( '|' ); apiArgs.iiurlwidth = requestedWidth; if ( this.imageInfo[filename] === undefined ) { // Fetch all image info in the same API query, save a request later apiArgs.iiprop = iiprops.join( '|' ); this.imageInfo[filename] = makeImageInfoRequest( apiArgs ); return this.imageInfo[filename]; } else if ( props.indexOf( 'url' ) === -1 ) { // Just return the cached promise, because we don't need to // fetch this information again. return this.imageInfo[filename]; } else { // Fetch the new thumb url but nothing else, because it's // the only non-cacheable thing apiArgs.iiprop = 'url'; return this.imageInfo[filename].then( function ( imageData, repoInfo ) { var maybeThumb = imageData.getThumbUrl( requestedWidth ); // Thumbnail caching! Woo! if ( maybeThumb ) { return $.Deferred().resolve( imageData, repoInfo, targetWidth, requestedWidth ); } return viewer.api.get( apiArgs ).then( function ( data ) { var imageInfo, innerInfo; imageInfo = viewer.getFirst( data.query.pages ); innerInfo = viewer.getFirst( imageInfo.imageinfo ); if ( innerInfo.thumburl ) { imageData.addThumbUrl( innerInfo.thumbwidth, innerInfo.thumburl ); } return $.Deferred().resolve( imageData, repoInfo, targetWidth, requestedWidth ); } ); } ); } }; /** * Gets file usage info. * @param {mw.Title} fileTitle Title of the file page for the image. * @returns {jQuery.Promise.} a promise * resolving to a local and a global file usage object * FIXME should be parallel with the other fetches, or even better if it can be integrated * into the same API calls. Lets get it out first though. */ MMVP.fetchFileUsageInfo = function ( fileTitle ) { return $.when( this.imageUsageDataProvider.get( fileTitle ), this.globalUsageDataProvider.get( fileTitle ) ); }; /** * Gets all file-related info. * @param {mw.Title} fileTitle Title of the file page for the image. * @returns {jQuery.Promise.} */ MMVP.fetchImageInfoAndFileUsageInfo = function ( fileTitle ) { return $.when( this.fetchImageInfo( fileTitle ), this.fetchFileUsageInfo( fileTitle ) ).then( function( first, second ) { var d = $.Deferred(); return d.resolve.apply( d, first.concat( second ) ); } ); }; MMVP.loadIndex = function ( index ) { var $clicked = $( imgsSelector ).eq( index ); if ( index < this.lightbox.images.length && index >= 0 ) { this.loadImage( this.lightbox.images[index], $clicked.prop( 'src' ) ); } }; MMVP.nextImage = function () { this.loadIndex( this.lightbox.currentIndex + 1 ); }; MMVP.prevImage = function () { this.loadIndex( this.lightbox.currentIndex - 1 ); }; MMVP.log = function ( action ) { mw.log( mmvLogActions[action] || action ); if ( mw.eventLog ) { mw.eventLog.logEvent( 'MediaViewer', { version: '1.1', action: action } ); } }; ( function () { var profiling = {}, nonce = 0; /** * Start profiling an event * @param {string} type Can be image-load, image-resize, metadata-fetch, gender-fetch * @param {Object} [imgSize] Size of image (for image related events) * @param {number} [imgSize.width] In pixels * @param {number} [imgSize.height] In pixels * @param {number} [imgSize.filesize] In bytes * @param {string} [fileType] File type (for image related events) * @param {number} [timeout] Optional timeout for the event. * @returns {number} The id used to finish the profiling */ MMVP.profileStart = function ( type, imgSize, fileType, timeout ) { var thisid = nonce++; imgSize = imgSize || {}; profiling[thisid] = { /* Changelog: * 1.1 fixed the issue with zeros in every message * 1.0 first release */ version: '1.1', action: type, imageWidth: imgSize.width, imageHeight: imgSize.height, fileSize: imgSize.filesize, fileType: fileType, userAgent: navigator.userAgent, start: Date.now() }; mw.log( mmvLogActions['profile-' + type + '-start'].replace( /\$1/g, thisid ) ); if ( timeout ) { window.setTimeout( function () { profiling[thisid] = undefined; }, timeout ); } return thisid; }; /** * Signal the end of an event being profiled and send the * eventlogging message. * @param {number} id Should be the value returned from profileStart * @param {boolean} [fakeTime] For testing, whether to fake the time in the message. Time is zero if so. */ MMVP.profileEnd = function ( id, fakeTime ) { var msg; if ( profiling[id] ) { msg = profiling[id]; msg.milliseconds = Date.now() - msg.start; delete msg.start; mw.log( mmvLogActions['profile-' + msg.action + '-end'] .replace( /\$1/g, id ) .replace( /\$2/g, fakeTime ? 0 : msg.milliseconds ) ); if ( mw.eventLog ) { mw.eventLog.logEvent( 'MediaViewerPerf', msg ); } } }; }() ); /** * Transforms a date string into localized, human-readable format. * Unrecognized strings are returned unchanged. * @param {string} dateString * @return {string} */ MMVP.formatDate = function ( dateString ) { var date = moment( dateString ); if ( !date.isValid() ) { return dateString; } return date.format( 'LL' ); }; function handleHash() { var statedIndex, $foundElement, hash = decodeURIComponent( document.location.hash ), linkState = hash.split( '/' ); comingFromPopstate = true; if ( linkState[0] === '#mediaviewer' ) { statedIndex = mw.mediaViewer.lightbox.images[linkState[2]]; if ( statedIndex.filePageTitle.getPrefixedText() === linkState[1] ) { $foundElement = $( imgsSelector ).eq( linkState[2] ); mw.mediaViewer.loadImage( statedIndex, $foundElement.prop( 'src' ) ); } } else { // If the hash is invalid (not a mmv hash) we check if there's any mmv lightbox open and we close it if ( mw.mediaViewer && mw.mediaViewer.lightbox && mw.mediaViewer.lightbox.iface ) { mw.mediaViewer.lightbox.iface.unattach(); } } } $( function () { MultiLightbox = window.MultiLightbox; lightboxHooks = window.lightboxHooks; var viewer = new MultimediaViewer(); mw.mediaViewer = viewer; handleHash(); window.addEventListener( 'popstate', handleHash ); } ); mw.MultimediaViewer = MultimediaViewer; }( mediaWiki, jQuery, moment ) );