mediawiki-extensions-Multim.../resources/ext.multimediaViewer/ext.multimediaViewer.js
Mark Holmquist 85eb742277 Previous and next functionality!
Adds buttons on the side, disabled at the start and end of the list of
images, that let you browse through the images on the page!

Also arrow keys!

EXCITING!

Change-Id: Ie514cd4b16bdee8a384a89d4bd1d755a24c5cdb4
2013-11-26 21:10:16 +00:00

762 lines
19 KiB
JavaScript
Executable file

/*
* 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, $, 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.'
};
function MultimediaViewer() {
var $thumbs = $( '.gallery .image img, a.image img' ),
urls = [],
viewer = this;
this.api = new mw.Api();
this.imageInfo = {};
$thumbs.each( function ( i, thumb ) {
var fileLink, thisImage,
$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' ) ) {
$thumbContain = $thumbContain.find( '.image' );
}
$links.data( 'filePageLink', filePageLink );
thisImage = new mw.LightboxImage( fileLink );
thisImage.filePageLink = filePageLink;
thisImage.filePageTitle = fileTitle;
thisImage.index = index;
urls.push( thisImage );
$links.click( function ( e ) {
// 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 $this = $( this ),
initial = $thumbContain.find( 'img' ).prop( 'src' );
if ( $this.is( 'a.image' ) ) {
viewer.log( 'thumbnail-link-click' );
} else if ( $this.is( '.magnify a' ) ) {
viewer.log( 'enlarge-link-click' );
}
e.preventDefault();
viewer.loadImage( thisImage, initial );
return false;
} );
} );
if ( $thumbs.length > 0 ) {
this.lightbox = new MultiLightbox( urls );
}
lightboxHooks.register( 'closeInterface', function () {
this.$mwControls.css( { top: '-999px', left: '-999px' } );
this.$nextButton.add( this.$prevButton ).css( 'top', '-999px' );
$( document.body ).removeClass( 'mw-mlb-lightbox-open' );
if ( comingFromPopstate === false ) {
history.pushState( {}, '', '#' );
} else {
comingFromPopstate = false;
}
} );
lightboxHooks.register( 'imageResize', function () {
var ui = this;
viewer.resize( ui );
return false;
} );
lightboxHooks.register( 'fullscreen', function () {
if ( this.$imageMetadata ) {
this.$imageMetadata.hide();
}
} );
lightboxHooks.register( 'defullscreen', function () {
if ( this.$imageMetadata ) {
this.$imageMetadata.show();
}
} );
lightboxHooks.register( 'clearInterface', function () {
this.$license.empty().addClass( 'empty' );
this.$imageDesc.empty();
this.$imageDescDiv.addClass( 'empty' );
this.$title.empty();
this.$credit.empty().addClass( 'empty' );
this.$username.empty();
this.$usernameLi.addClass( 'empty' );
this.$repo.empty();
this.$repoLi.addClass( 'empty' );
this.$datetime.empty();
this.$datetimeLi.addClass( 'empty' );
this.$useFile.data( 'title', null );
this.$useFile.data( 'link', null );
this.$useFile.data( 'src', null );
this.$useFileLi.addClass( 'empty' );
this.$imageDiv.addClass( 'empty' );
} );
}
MMVP = MultimediaViewer.prototype;
MMVP.resize = function ( ui ) {
var api = new mw.Api(),
viewer = this,
density = $.devicePixelRatio(),
filename = ui.currentImageFilename;
api.get( {
action: 'query',
format: 'json',
titles: filename,
prop: 'imageinfo',
iiprop: 'url',
iiurlwidth: Math.floor( density * ui.$imageWrapper.width() ),
iiurlheight: Math.floor( density * ui.$imageWrapper.height() )
} ).done( function ( data ) {
var imageInfo, innerInfo,
image = new Image();
$.each( data.query.pages, function ( i, page ) {
imageInfo = page;
return false;
} );
innerInfo = imageInfo.imageinfo[0];
image.onload = function () {
ui.replaceImageWith( image );
viewer.updateControls();
};
image.src = innerInfo.thumburl || innerInfo.url;
} );
};
MMVP.updateControls = function () {
var isOnButton = false,
isOnImage = false,
ui = this.ui,
pos = ui.$image.offset(),
prevNextTop = ( ( ui.$imageWrapper.height() / 2 ) - 32 ) + 'px';
function fadeIn() {
isOnImage = true;
ui.$mwControls.fadeIn( 100 );
ui.$image.one( 'click', fadeOut );
}
function fadeOut() {
ui.$mwControls.fadeOut( 100 );
ui.$image.one( 'click', fadeIn );
}
function fadeOutDelayed() {
isOnImage = false;
setTimeout( function () {
if ( !isOnImage && !isOnButton ) {
fadeOut();
}
}, 500 );
}
function detectButton() {
isOnButton = true;
}
function detectLeaveButton() {
isOnButton = false;
setTimeout( function () {
if ( !isOnImage && !isOnButton ) {
fadeOut();
}
}, 500 );
}
pos.top = ( ui.$imageWrapper.height() - ui.$image.height() ) / 2;
pos.left += ui.$image.width() - ui.$closeButton.width();
pos.top += 'px';
pos.left += 'px';
ui.$mwControls
.css( pos )
.appendTo( ui.$main )
.fadeIn( 100 )
.delay( 500 )
.fadeOut( 100 );
ui.$postDiv.css( 'top', ui.$imageWrapper.height() );
ui.$image
.off( 'mouseenter', fadeIn )
.off( 'mouseleave', fadeOutDelayed )
.one( 'click', fadeIn )
.on( 'mouseenter', fadeIn )
.on( 'mouseleave', fadeOutDelayed );
ui.$closeButton.add( ui.$fullscreenButton )
.off( 'mouseenter', detectButton )
.off( 'mouseleave', detectLeaveButton )
.on( 'mouseenter', detectButton )
.on( 'mouseleave', detectLeaveButton );
ui.$nextButton.add( ui.$prevButton ).css( {
top: prevNextTop
} );
ui.$nextButton.toggleClass( 'disabled', this.lightbox.currentIndex >= ( this.lightbox.images.length - 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.fetchRepoInfo = function ( cb ) {
var viewer = this;
if ( this.repoInfo !== undefined ) {
cb( this.repoInfo );
} else {
this.api.get( {
action: 'query',
format: 'json',
meta: 'filerepoinfo'
}, function ( data ) {
if ( !data || !data.query ) {
// Damn, failure. Do it gracefully-ish.
cb( {} );
return;
}
viewer.setRepoInfo( data.query.repos );
cb( viewer.repoInfo );
} );
}
};
MMVP.setRepoInfo = 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;
}
};
MMVP.setImageInfo = function ( fileTitle, imageInfo ) {
function whitelistHtml( $el ) {
var child, $prev, $child = $el.children().first();
while ( $child && $child.length ) {
child = $child.get( 0 );
if ( child.nodeType !== child.ELEMENT_NODE ) {
return;
}
whitelistHtml( $child );
if ( !$child.is( 'a' ) ) {
$prev = $child.prev();
$child.replaceWith( $child.contents() );
} else {
$prev = $child;
}
if ( $prev && $prev.length === 1 ) {
$child = $prev.next();
} else {
$child = $el.children().first();
}
}
}
function setUserpageLink( username, gender ) {
var userpage = 'User:' + username,
userTitle = mw.Title.newFromText( userpage );
ui.$username
.text(
mw.message( 'multimediaviewer-userpage-link', username, gender ).text()
)
.prop( 'href', userTitle.getUrl() );
if ( articlePath ) {
ui.$username
.prop( 'href', articlePath.replace( '$1', userTitle.getPrefixedText() ) );
}
ui.$usernameLi.toggleClass( 'empty', !Boolean( username ) );
}
var extmeta,
repoInfo, articlePath, linkToRepo,
desc,
datetime, dtmsg,
license, msgname,
username,
source, author,
ui = this.lightbox.iface,
innerInfo = imageInfo.imageinfo[0] || {};
ui.$title.text( fileTitle.getNameText() );
ui.$useFile.data( 'title', fileTitle );
ui.$useFile.data( 'src', innerInfo.url );
ui.$useFileLi.removeClass( 'empty' );
if ( this.repoInfo ) {
repoInfo = this.repoInfo[imageInfo.imagerepository];
}
if ( repoInfo ) {
if ( repoInfo.displayname ) {
ui.$repo.text(
mw.message( 'multimediaviewer-repository', repoInfo.displayname ).text()
);
} else {
ui.$repo.text(
mw.message( 'multimediaviewer-repository', mw.config.get( 'wgSiteName' ) ).text()
);
}
if ( repoInfo.server && repoInfo.articlepath ) {
articlePath = repoInfo.server + repoInfo.articlepath;
} else {
articlePath = mw.config.get( 'wgArticlePath' );
}
linkToRepo = articlePath.replace( '$1', fileTitle.getPrefixedText() );
if ( repoInfo.local ) {
linkToRepo = mw.config.get( 'wgServer' ) + linkToRepo;
}
ui.$repo.prop( 'href', linkToRepo );
ui.$useFile.data( 'link', linkToRepo );
}
ui.$repoLi.toggleClass( 'empty', !Boolean( repoInfo ) );
username = innerInfo.user;
if ( username ) {
// 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: repoInfo.apiurl || mw.util.wikiScript( 'api' )
}
} ).get( {
action: 'query',
list: 'users',
ususers: username,
usprop: 'gender'
} ).done( function ( data ) {
var gender = data.query.users[0].gender;
setUserpageLink( username, gender );
} ).fail( function () {
setUserpageLink( username, 'unknown' );
} );
}
extmeta = innerInfo.extmetadata;
if ( extmeta ) {
desc = extmeta.ImageDescription;
ui.$imageDescDiv.toggleClass( 'empty', !desc );
if ( desc ) {
desc = desc.value;
whitelistHtml( ui.$imageDesc.append( $.parseHTML( desc ) ) );
}
datetime = extmeta.DateTimeOriginal || extmeta.DateTime;
if ( datetime ) {
// get rid of HTML tags
datetime = datetime.value.replace( /<.*?>/g, '' );
datetime = this.formatDate( datetime );
dtmsg = (
'multimediaviewer-datetime-' +
( extmeta.DateTimeOriginal ? 'created' : 'uploaded' )
);
ui.$datetime.text(
mw.message( dtmsg, datetime ).text()
);
}
ui.$datetimeLi.toggleClass( 'empty', !Boolean( datetime ) );
source = extmeta.Credit;
author = extmeta.Artist;
if ( source ) {
source = source.value;
whitelistHtml( ui.$source.empty().append( $.parseHTML( source ) ) );
}
if ( author ) {
author = author.value;
whitelistHtml( ui.$author.empty().append( $.parseHTML( author ) ) );
}
if ( source && 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 ( source ) {
ui.$credit.html( ui.$source );
} else if ( author ) {
ui.$credit.html( ui.$author );
}
}
ui.$credit.toggleClass( 'empty', !Boolean( source ) && !Boolean( author ) );
license = extmeta.License;
}
if ( license ) {
license = license.value;
}
msgname = 'multimediaviewer-license-' + ( license || '' );
if ( !license || !mw.messages.exists( msgname ) ) {
// Cannot display, fallback or fail
license = 'default';
msgname = 'multimediaviewer-license-default';
}
if ( license ) {
articlePath = articlePath || mw.config.get( 'wgArticlePath', '' );
ui.$license
.text( mw.message( msgname ).text() )
.prop( 'href', articlePath.replace( '$1', fileTitle.getPrefixedText() ) )
.toggleClass( 'cc-license', license.substr( 0, 2 ) === 'cc' );
}
ui.$license.toggleClass( 'empty', !license );
};
MMVP.loadImage = function ( image, initialSrc ) {
var 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.src = initialSrc;
this.currentImageFilename = image.filePageTitle.getPrefixedText();
this.lightbox.iface.comingFromPopstate = comingFromPopstate;
this.lightbox.open();
$( document.body ).addClass( 'mw-mlb-lightbox-open' );
this.lightbox.iface.$imageDiv.append( $.createSpinner( {
id: 'mw-mlb-loading-spinner',
size: 'large'
} ) );
this.fetchImageInfo( image.filePageTitle, function ( imageInfo ) {
var imageEle = new Image();
imageEle.onload = function () {
viewer.lightbox.iface.replaceImageWith( imageEle );
viewer.lightbox.iface.$imageDiv.removeClass( 'empty' );
viewer.updateControls();
$.removeSpinner( 'mw-mlb-loading-spinner' );
viewer.setImageInfo( image.filePageTitle, imageInfo );
};
imageEle.src = imageInfo.imageinfo[0].thumburl || imageInfo.imageinfo[0].url;
} );
comingFromPopstate = false;
};
MMVP.fetchImageInfo = function ( fileTitle, cb ) {
function apiCallback( sitename ) {
return function ( data ) {
if ( !data || !data.query ) {
// No information, oh well
return;
}
viewer.setRepoInfo( data.query.repos );
if ( data.query.pages ) {
$.each( data.query.pages, function ( i, page ) {
imageInfo = page;
return false;
} );
if ( viewer.imageInfo[filename] === undefined ) {
if ( sitename === null ) {
viewer.imageInfo[filename] = imageInfo;
} else {
viewer.imageInfo[filename] = {};
}
viewer.imageInfo[filename].sites = {};
if ( !viewer.imageInfo[filename].imageinfo ||
viewer.imageInfo[filename].imageinfo.length === 0 ) {
viewer.imageInfo[filename].imageinfo = [{}];
}
}
viewer.imageInfo[filename].sites[sitename] = imageInfo;
}
if ( viewer.imageInfo[filename] ) {
// Give back the information we have
cb( viewer.imageInfo[filename], viewer.repoInfo );
}
};
}
function fetchImageInfoCallback() {
var repoInfo;
if ( viewer.repoInfo !== undefined ) {
repoInfo = viewer.repoInfo;
}
if ( repoInfo === undefined ) {
apiArgs.meta = 'filerepoinfo';
}
viewer.api.get( apiArgs ).done( apiCallback( null ) );
}
var imageInfo,
filename = fileTitle.getPrefixedText(),
density = $.devicePixelRatio(),
apiArgs = {
action: 'query',
format: 'json',
titles: filename,
prop: 'imageinfo',
iiprop: iiprops.join( '|' ),
iiurlwidth: Math.floor( density * this.lightbox.iface.$imageWrapper.width() ),
iiurlheight: Math.floor( density * this.lightbox.iface.$imageWrapper.height() ),
// 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;
if ( this.imageInfo[filename] === undefined ) {
// Fetch it in the same API query as the image info
fetchImageInfoCallback();
} else {
this.fetchRepoInfo( function ( res ) {
cb( viewer.imageInfo[filename], res );
} );
}
};
MMVP.loadIndex = function ( index ) {
if ( index < this.lightbox.images.length && index >= 0 ) {
this.loadImage( this.lightbox.images[index], $( imgsSelector ).eq( index ).prop( 'src' ) );
this.resize( this.lightbox.iface );
}
};
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.0',
action: action,
userId: mw.user.getId(),
editCount: mw.config.get( 'wgUserEditCount', 0 )
} );
}
};
/**
* Transforms a date string into localized, human-readable format.
* Unrecognized strings are returned unchanged.
* @param {string} dateString
* @return {string}
*/
MultimediaViewer.prototype.formatDate = function ( dateString ) {
var date = moment( dateString );
if ( !date.isValid() ) {
return dateString;
}
return date.format( 'LL' );
};
function handleHash( hash ) {
var statedIndex, linkState = hash.split( '/' );
comingFromPopstate = true;
if ( linkState[0] === '#mediaviewer' ) {
statedIndex = mw.mediaViewer.lightbox.images[linkState[2]];
if ( statedIndex.filePageTitle.getPrefixedText() === linkState[1] ) {
mw.mediaViewer.loadImage( statedIndex, $( imgsSelector ).eq( linkState[2] ).prop( 'src' ) );
}
} else {
mw.mediaViewer.lightbox.iface.unattach();
}
}
$( function () {
MultiLightbox = window.MultiLightbox;
lightboxHooks = window.lightboxHooks;
var viewer = new MultimediaViewer();
mw.mediaViewer = viewer;
handleHash( document.location.hash );
window.addEventListener( 'popstate', function () { handleHash( document.location.hash ); } );
} );
mw.MultimediaViewer = MultimediaViewer;
// Quick hack to select all text in a text box
$.fn.selectAll = function () {
return this.each( function () {
var range,
start = 0,
end = this.value.length;
if ( this.setSelectionRange ) {
this.setSelectionRange( start, end );
} else if ( this.createTextRange ) {
range = this.createTextRange();
range.collapse( true );
range.moveEnd( 'character', end );
range.moveStart( 'character', start );
range.select();
}
} );
};
}( mediaWiki, jQuery, moment ) );