/* ==================================================================
* jump_to_file.js
*
* Adds a link from Page to the transcluding mainspace page
*
* Adds a link to the file at Commons from a Page: namespace page,
* an Index: page or a mainspace page with a "source" tab
*
* Adds a link to the work and hi-res page image at source (e.g. IA) if possible.
*
* Configuration is via "High-res options" in the sidebar.
* ================================================================== */
/* eslint-disable
camelcase, no-use-before-define, no-var
*/
( function () {
'strict';
// reference pre-loaded libraries from global scope
var getGlobalLibrary = function ( key ) {
var scriptName = window.ScriptDeps && window.ScriptDeps[ key ] || key;
return window[ scriptName ];
};
var debugSuffix = '';
/* debug-replace: debugSuffix = '.debug'; */
var ILUI = getGlobalLibrary( 'iltools.ui' + debugSuffix );
/* ======================================================================== */
// MW Options and LocalStorage JSON interfaces
var persistUserOption = function ( key, data ) {
new mw.Api().saveOption( 'userjs-' + key, JSON.stringify( data ) );
mw.user.options.set( key, data );
};
var getUserOption = function ( key ) {
var data = mw.user.options.get( 'userjs-' + key ) || '';
try {
data = JSON.parse( data );
} catch ( e ) {
data = {};
}
return data;
};
var persistLocalStorage = function ( key, data ) {
window.localStorage.setItem( 'userjs-' + key, JSON.stringify( data ) );
};
var getLocalStorage = function ( key ) {
var lsData = window.localStorage.getItem( 'userjs-' + key ) || '';
try {
lsData = JSON.parse( lsData );
} catch ( e ) {
lsData = {};
}
return lsData;
};
/* ======================================================================== */
var JumpToFile = {
signature: 'JumpToFile',
strings: {
link_title_pat: 'Page transcluded in $1',
go_to_file_commons: 'Go to file at Commons',
go_to_file_ns: 'Go to file in File namespace',
visit_pat: 'Visit document at $1',
portlet_title: 'Links',
portlet_id: 'p-jumptofile',
hiResOptions: 'High-res options',
hiResOptionsTip: 'Change settings for download of high-res options'
},
state: {
offset: 0,
loadHiRes: false
},
// for things that probably don't change often, allow to cache the results
// for a short while to speed up scrubbing though pages
maxage: 120,
pageIndex: getPageIndex(),
transcludeIcon: '//upload.wikimedia.org/wikipedia/commons/thumb/9/92/Open_book_nae_02.svg/25px-Open_book_nae_02.svg.png',
pageAnchorPrefix: 'pageindex_',
useHighresImg: true,
infoHostname: 'https://pagelister.toolforge.org',
fileIcon: '//upload.wikimedia.org/wikipedia/commons/5/56/Book_go.png',
showIiifLinks: false
};
function getPageIndex() {
if ( mw.config.get( 'wgCanonicalNamespace' ) === 'Page' ) {
var num = mw.config.get( 'wgPageName' ).substring( mw.config.get( 'wgPageName' ).lastIndexOf( '/' ) + 1 );
return parseInt( num );
}
return null;
}
function addLinks(links) {
for (let i = 0; i < links.length; ++i) {
let portletLink = mw.util.addPortletLink(
JumpToFile.strings.portlet_id,
links[i].href,
' ' + links[i].title,
links[i].id
);
if (links[i].icon) {
$(portletLink).find('a').prepend(
$('<img>')
.attr({
src: links[i].icon,
height: 16
})
);
}
if (links[i].class) {
// The following classes are used here:
// * srcimglink
// * transclusion-link
$(portletLink).find('a').addClass(links[i].class);
}
}
}
function api_get_tranclusions( namespaces, callback ) {
var ns_str = namespaces.join( '|' ),
api = new mw.Api();
api.get( {
action: 'query',
format: 'json',
list: 'embeddedin',
einamespace: ns_str,
eititle: mw.config.get( 'wgPageName' )
} ).done( function ( data ) {
callback( data.query.embeddedin );
} );
}
/*
* Calls the given callback with true if the image is shared
*/
function apiCheckRepository( filename, callback ) {
var api = new mw.Api();
api.get( {
action: 'query',
format: 'json',
formatversion: 2,
maxage: JumpToFile.maxage,
prop: 'imageinfo',
titles: 'File:' + filename,
iiprop: ''
} ).done( function ( data ) {
callback( data.query.pages[ 0 ].imagerepository );
} );
}
/**
* Reject bogus mainspace transclusions (e.g. the page is used in
* a progress bar)
*
* @param {string} title
* @return {boolean}
*/
function isValidTransclusion( title ) {
var base = title.split( '/' )[ 0 ];
return base !== 'Main Page';
}
/**
* Set up link to pages which transclude this one
*/
function setUpTransclusionLinks() {
if ( mw.config.get( 'wgCanonicalNamespace' ) !== 'Page' ) {
return;
}
api_get_tranclusions( [ 0, 114 ], function ( embeddedin ) {
var links = [];
for ( var i = 0; i < embeddedin.length; ++i ) {
if ( !isValidTransclusion( embeddedin[ i ].title ) ) {
continue;
}
var href = mw.util.getUrl( embeddedin[ i ].title ) +
'#' + JumpToFile.pageAnchorPrefix + getPageIndex(),
link = {
id: 'ca-ns0_' + i,
href: href,
icon: JumpToFile.transcludeIcon,
title: JumpToFile.strings.link_title_pat.replace( '$1', embeddedin[ i ].title ),
class: 'transclusion-link'
};
links.push( link );
}
addLinks( links );
} );
}
function setUpFileLinks() {
var filename,
ns = mw.config.get( 'wgCanonicalNamespace' );
if ( ns === 'Index' ) {
filename = mw.config.get( 'wgTitle' );
} else if ( ns === 'Page' ) {
filename = mw.config.get( 'wgTitle' ).replace( /\/\d+$/, '' );
}
if ( !filename ) {
// look in the "source" tab
// eslint-disable-next-line no-jquery/no-global-selector
var $link = $( '#ca-proofread-source a' );
if ( $link.length ) {
filename = $link.first().attr( 'href' );
filename = filename.substring( filename.indexOf( ':' ) + 1 );
}
}
set_up_link_from_file( filename );
}
function set_up_link_from_file( filename ) {
apiCheckRepository( filename, function ( repo ) {
var link_href = '';
var shared = [ 'shared', 'wikimediacommons' ].indexOf( repo ) !== -1;
if ( shared ) {
link_href = '//commons.wikimedia.org/wiki/File:' + filename;
} else {
link_href = mw.config.get( 'wgArticlePath' ).replace( '$1', 'File:' + filename );
}
addLinks( [ {
icon: JumpToFile.fileIcon,
href: link_href,
title: shared ?
JumpToFile.strings.go_to_file_commons : JumpToFile.strings.go_to_file_ns,
class: 'srcimglink'
} ] );
} );
var url = new URL( JumpToFile.infoHostname + '/img_links/v1/links' );
url.searchParams.append( 'file', filename );
url.searchParams.append( 'page', JumpToFile.pageIndex + JumpToFile.state.offset );
fetch( url )
.then( function ( data ) {
return data.json();
} )
.then( function ( jsonData ) {
handle_file_links( jsonData );
} );
}
function handle_file_links( linkData ) {
if ( !linkData.links ) {
return;
}
let highRes;
// Add links to tab
const displayLinks = linkData.links
.filter( ( link ) => {
// IIIF minfest not very useful to users in this menu
if ( [ 'iiif', 'iiif-manifest' ].indexOf( link.type ) !== -1 ) {
return JumpToFile.showIiifLinks;
}
// show everything else
return true;
} )
.map( ( link ) => {
return {
icon: link.icon,
href: link.url,
title: link.title,
class: 'srcimglink'
};
} );
addLinks( displayLinks );
for ( const link of linkData.links ) {
if ( link.highres === true && !highRes ) {
highRes = link;
} else if ( [ 'dzi', 'iiif' ].indexOf( link.type ) !== -1 ) {
highRes = link;
}
}
if ( highRes ) {
setHighresImg( highRes );
}
}
let originalItemCount;
function setHighresImg( highres ) {
// no viewer, nothing to do
if ( !mw.proofreadpage || !mw.proofreadpage.openseadragon || !mw.proofreadpage.openseadragon.viewer ) {
return;
}
if ( JumpToFile.state.loadHighres &&
highres && ( mw.config.get( 'wgCanonicalNamespace' ) === 'Page' ) ) {
if ( originalItemCount === undefined ) {
originalItemCount = mw.proofreadpage.openseadragon.viewer.world.getItemCount();
}
if ( highres.type === 'iiif' || highres.type === 'dzi' ) {
mw.proofreadpage.openseadragon.viewer
.addTiledImage( {
// load with the old image in the background to avoid a large flicker
preload: true,
tileSource: highres.data || highres.url
} );
} else if ( highres.type === 'image' ) {
mw.proofreadpage.openseadragon.viewer
.addSimpleImage( {
replace: false,
url: highres.url
} );
}
mw.hook( JumpToFile.signature + '.highres_set' ).fire( highres );
} else {
// remove any high res images
const currCount = mw.proofreadpage.openseadragon.viewer.world.getItemCount();
for ( let i = currCount - 1; i >= originalItemCount; --i ) {
const item = mw.proofreadpage.openseadragon.viewer.world.getItemAt( i );
mw.proofreadpage.openseadragon.viewer.world.removeItem( item );
}
}
}
var initWindowManager = function () {
if ( JumpToFile.windowManager ) {
return;
}
JumpToFile.windowManager = new OO.ui.WindowManager();
// Create and append a window manager, which will open and close the window.
$( document.body ).append( JumpToFile.windowManager.$element );
};
var getIndexKey = function () {
return mw.config.get( 'wgTitle' ).replace( /\/\d+$/, '' );
};
var initialiseLsData = function ( lsData ) {
if ( !lsData.offsets ) {
lsData.offsets = {};
}
};
var loadSettings = function () {
var opts = getUserOption( JumpToFile.signature );
var lsData = getLocalStorage( JumpToFile.signature );
initialiseLsData( lsData );
loadSettingsFromData( opts, lsData );
};
var loadSettingsFromData = function ( opts, lsData ) {
JumpToFile.state.loadHighres = opts.loadHighres || false;
JumpToFile.state.offset = lsData.offsets[ getIndexKey() ] || 0;
};
var storeOptions = function ( params ) {
// these options save persistently across all sessions
var opts = {
loadHighres: params[ 0 ]
};
persistUserOption( JumpToFile.signature, opts );
// the offset opts go into local storage because they're big and can go stale
// TODO persist somewhere on the index page for all users?
var filename = getIndexKey();
var lsData = getLocalStorage( JumpToFile.signature );
initialiseLsData( lsData );
lsData.offsets[ filename ] = params[ 1 ];
persistLocalStorage( JumpToFile.signature, lsData );
loadSettingsFromData( opts, lsData );
reloadFileLinks();
return true;
};
var reloadFileLinks = function () {
if ( JumpToFile.state.$linkUl ) {
JumpToFile.state.$linkUl.find( '.srcimglink' ).remove();
}
setUpFileLinks();
};
var showOptions = function () {
initWindowManager();
var needs = [
{
type: 'bool',
label: 'Load hi-res images',
help: 'Load high-resolution images from the upstream source if possible',
value: JumpToFile.state.loadHighres
},
{
type: 'int',
label: 'Source offset',
help: 'The offset between the page numbering in the WIkisource file and the file at the source. ' +
'Example: "0" if the files are identical, "1" if the source has a cover sheet and the Wikisource ' +
'file has had that page removed.',
value: JumpToFile.state.offset
}
];
// Make the window.
var dialog = new ILUI.GeneralParamsDialog( {
size: 'medium'
} );
JumpToFile.windowManager.addWindows( [ dialog ] );
JumpToFile.windowManager.openWindow( dialog, {
title: 'JumpToFile options',
needs: needs,
saveCallback: function ( params ) {
return $.Deferred().resolve( storeOptions( params ) );
}
} );
};
var addConfigMenu = function () {
var portlet = mw.util.addPortletLink( 'p-tb', '#',
JumpToFile.strings.hiResOptions, JumpToFile.signature + '-enable_hires_dl',
JumpToFile.strings.hiResOptionsTip );
$( portlet ).on( 'click', function ( e ) {
e.preventDefault();
showOptions();
} );
};
var addLinksMenu = function () {
const portlet = mw.util.addPortlet(
JumpToFile.strings.portlet_id,
JumpToFile.strings.portlet_title,
'#p-cactions'
);
const skin = mw.config.get("skin");
if (skin === 'vector') {
// We can use the returned node directly
$(portlet).appendTo("#left-navigation");
} else if (skin === 'vector-2022') {
// Need to grab the *-dropdown wrapper Vector 2022 creates
$('#' + JumpToFile.strings.portlet_id + "-dropdown")
.appendTo("#left-navigation");
} else {
// Do nothing since the skin doesn't support dropdowns anyway.
}
};
// The loader has handled "wait for DOM ready"
mw.hook( JumpToFile.signature + '.config' ).fire( JumpToFile );
loadSettings();
addConfigMenu();
addLinksMenu();
setUpTransclusionLinks();
reloadFileLinks();
}() );