MediaWiki:Common.js
MediaWiki interface page
More actions
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
( function () {
'use strict';
var GAR_NS = 3000; // NS_GAR
/* ==========================================================
Safety helpers
========================================================== */
// Neutralise wiki + HTML characters so a typed value cannot inject
// templates, links, parameters, or parser/HTML tags.
function escapeWikitext( str ) {
return String( str )
.replace( /\{/g, '{' )
.replace( /\}/g, '}' )
.replace( /\[/g, '[' )
.replace( /\]/g, ']' )
.replace( /\|/g, '|' )
.replace( /</g, '<' )
.replace( />/g, '>' );
}
// Escape $ so a value is safe inside a JS String.replace() replacement.
function safeReplacement( str ) {
return str.replace( /\$/g, '$$$$' );
}
// Reject a typed page name that contains title-illegal characters or a
// slash (which would silently create an off-structure subpage). Returns
// a cleaned name, or null if it must be refused.
function sanitizeName( name ) {
var n = String( name ).replace( /\s+/g, ' ' ).trim();
if ( !n ) {
return null;
}
if ( /[#<>\[\]{}|/]/.test( n ) ) {
return null;
}
return n;
}
// Filenames: same idea, but a filename legitimately may contain dots.
function sanitizeFilename( name ) {
var n = String( name ).replace( /\s+/g, ' ' ).trim();
if ( !n ) {
return null;
}
if ( /[#<>\[\]{}|]/.test( n ) ) {
return null;
}
return n;
}
// Strip a leading "Namespace:" from a title, leaving the bare path.
// (Titles come back from the API with spaces, e.g. "GAR:YouTube/A".)
function stripNs( title ) {
var t = String( title );
var idx = t.indexOf( ':' );
return idx > -1 ? t.slice( idx + 1 ) : t;
}
// If the create-prefix ends in a single A-Z letter, return it (else null).
function bucketLetterFromPrefix( prefix ) {
var p = String( prefix ).replace( /\/$/, '' );
var seg = p.split( '/' ).pop();
return /^[A-Za-z]$/.test( seg ) ? seg.toUpperCase() : null;
}
// First letter of a typed name, accents removed, upper-cased.
function firstLetter( name ) {
return name.trim().charAt( 0 ).normalize( 'NFD' ).replace( /[\u0300-\u036f]/g, '' ).toUpperCase();
}
// Zero-pad a case number to 11 digits: first case = Case-00000000001.
var CASE_DIGITS = 11;
function padCase( n ) {
var s = String( n );
while ( s.length < CASE_DIGITS ) {
s = '0' + s;
}
return s;
}
// Mimic MediaWiki's preload transform on raw template text:
// <onlyinclude> wins, <noinclude> removed, <includeonly> unwrapped.
// Lets preload templates be wrapped in <includeonly> (so the template
// page itself renders empty and joins no category) while the real page
// still receives clean wikitext.
function stripPreload( text ) {
var only = text.match( /<onlyinclude>([\s\S]*?)<\/onlyinclude>/gi );
if ( only ) {
text = only.map( function ( s ) {
return s.replace( /<\/?onlyinclude>/gi, '' );
} ).join( '' );
}
text = text.replace( /<noinclude>[\s\S]*?<\/noinclude>/gi, '' );
text = text.replace( /<\/?includeonly>/gi, '' );
return text.replace( /^\n+/, '' );
}
function markDone( $el ) {
if ( $el.data( 'garDone' ) ) {
return false;
}
$el.data( 'garDone', true );
return true;
}
/* ==========================================================
Live auto-updating grid. Lists real GAR pages by their
subpage structure (not categories), so nothing has to be
"registered" and template pages can never appear as tiles.
Two modes, set on the div:
data-listmode="platforms" -> top-level GAR pages (no "/"),
excluding the system pages.
data-listmode="children" -> the direct sub-pages of
data-parent (its immediate
children only, not grandchildren).
========================================================== */
var GAR_SYSTEM_PAGES = { 'Main': true, 'Offline': true };
function renderAutoGrid( $container ) {
if ( !markDone( $container ) ) {
return;
}
var mode = $container.data( 'listmode' ) || 'children';
$container.text( 'Loading...' );
mw.loader.using( 'mediawiki.api' ).then( function () {
var api = new mw.Api();
if ( mode === 'platforms' ) {
return api.get( {
action: 'query',
list: 'allpages',
apnamespace: GAR_NS,
aplimit: 'max',
formatversion: 2,
format: 'json'
} ).then( function ( data ) {
var pages = ( data.query && data.query.allpages ) || [];
var items = [];
pages.forEach( function ( p ) {
var path = stripNs( p.title );
if ( path.indexOf( '/' ) === -1 && !GAR_SYSTEM_PAGES[ path ] ) {
items.push( { title: p.title, label: path } );
}
} );
return items;
} );
}
// mode === 'children'
var parent = $container.data( 'parent' ) || mw.config.get( 'wgPageName' );
var parentPath = stripNs( String( parent ).replace( /_/g, ' ' ) );
var prefix = parentPath + '/';
return api.get( {
action: 'query',
list: 'allpages',
apnamespace: GAR_NS,
apprefix: prefix,
aplimit: 'max',
formatversion: 2,
format: 'json'
} ).then( function ( data ) {
var pages = ( data.query && data.query.allpages ) || [];
var items = [];
pages.forEach( function ( p ) {
var path = stripNs( p.title );
if ( path.indexOf( prefix ) === 0 ) {
var rest = path.slice( prefix.length );
if ( rest && rest.indexOf( '/' ) === -1 ) {
items.push( { title: p.title, label: rest } );
}
}
} );
return items;
} );
} ).then( function ( items ) {
$container.empty();
if ( !items || items.length === 0 ) {
$container.append( $( '<p>' ).text( 'Nothing has been added here yet.' ) );
return;
}
items.sort( function ( a, b ) {
return a.label.localeCompare( b.label );
} );
items.forEach( function ( it ) {
$container.append(
$( '<a>' ).attr( 'href', mw.util.getUrl( it.title ) ).text( it.label )
);
} );
}, function () {
$container.text( 'Could not load this list right now.' );
} );
}
/* ==========================================================
Instant-create core
========================================================== */
function fallbackToManualEditor( newTitle, preloadTitle, $status, reason ) {
console.error( 'GAR instant-create fell back to manual editor. Reason: ' + reason + '. Title: ' + newTitle );
$status.text( ' Opening the normal editor instead...' );
window.location.href = mw.util.getUrl( newTitle, { action: 'edit', preload: preloadTitle } );
}
function applyExtraFields( rawText, fields ) {
var text = rawText;
if ( fields.image ) {
text = text.replace( /\|image(\s*)=\s*\n/, function ( match, spacing ) {
return '|image' + spacing + '= ' + safeReplacement( escapeWikitext( fields.image ) ) + '\n';
} );
}
if ( fields.evidence ) {
text = text.replace( '<Add Picture Here>', safeReplacement( escapeWikitext( fields.evidence ) ) );
}
if ( fields.description ) {
text = text.replace( '<Write Text Here>', safeReplacement( escapeWikitext( fields.description ) ) );
}
return text;
}
function runInstantCreate( newTitle, preloadTitle, fields, $status ) {
$status.text( ' Creating...' );
mw.loader.using( 'mediawiki.api' ).then( function () {
var api = new mw.Api();
api.get( {
action: 'query',
titles: newTitle,
formatversion: 2,
format: 'json'
} ).then( function ( data ) {
var pages = ( data.query && data.query.pages ) || [];
var exists = false;
pages.forEach( function ( page ) {
if ( !page.missing ) {
exists = true;
}
} );
if ( exists ) {
$status.text( ' Already exists, opening it...' );
window.location.href = mw.util.getUrl( newTitle );
return;
}
api.get( {
action: 'query',
titles: preloadTitle,
prop: 'revisions',
rvprop: 'content',
rvslots: 'main',
formatversion: 2,
format: 'json'
} ).then( function ( tplData ) {
var tplPages = ( tplData.query && tplData.query.pages ) || [];
var rawText = '';
tplPages.forEach( function ( page ) {
if ( page.revisions && page.revisions[ 0 ] && page.revisions[ 0 ].slots && page.revisions[ 0 ].slots.main ) {
rawText = page.revisions[ 0 ].slots.main.content;
}
} );
if ( !rawText ) {
fallbackToManualEditor( newTitle, preloadTitle, $status, 'preload template came back empty' );
return;
}
var finalText = applyExtraFields( stripPreload( rawText ), fields );
api.postWithToken( 'csrf', {
action: 'edit',
title: newTitle,
text: finalText,
createonly: true,
formatversion: 2,
summary: 'Auto-created'
} ).then( function ( editResult ) {
if ( editResult && editResult.edit && editResult.edit.result === 'Success' ) {
window.location.href = mw.util.getUrl( newTitle );
} else {
fallbackToManualEditor( newTitle, preloadTitle, $status, 'edit call did not report Success' );
}
}, function ( code ) {
if ( code === 'articleexists' ) {
window.location.href = mw.util.getUrl( newTitle );
return;
}
fallbackToManualEditor( newTitle, preloadTitle, $status, 'edit call failed: ' + code );
} );
}, function ( code ) {
fallbackToManualEditor( newTitle, preloadTitle, $status, 'template fetch failed: ' + code );
} );
}, function ( code ) {
fallbackToManualEditor( newTitle, preloadTitle, $status, 'existence check failed: ' + code );
} );
} );
}
/* ==========================================================
Typed-name forms (platforms, countries, cities, people)
========================================================== */
function buildForm( $container ) {
if ( !markDone( $container ) ) {
return;
}
var prefix = $container.data( 'prefix' );
var preloadTitle = $container.data( 'preload' );
var placeholder = $container.data( 'placeholder' ) || 'Enter a name';
var buttonLabel = $container.data( 'buttonlabel' ) || 'Add';
var extraFieldsRaw = $container.data( 'extrafields' ) || '';
var $input = $( '<input>' ).attr( 'type', 'text' ).attr( 'placeholder', placeholder ).css( 'width', '100%' ).css( 'margin-bottom', '6px' );
var $wrapper = $( '<div>' ).append( $input );
var extraInputs = {};
var plainExtraInputs = [];
if ( extraFieldsRaw ) {
extraFieldsRaw.split( ';' ).forEach( function ( pair ) {
var parts = pair.split( ':' );
var key = $.trim( parts[ 0 ] );
var label = $.trim( parts[ 1 ] || key );
var $extra;
if ( key === 'description' ) {
$extra = $( '<textarea>' ).attr( 'placeholder', label ).attr( 'rows', 3 ).css( 'width', '100%' ).css( 'margin-bottom', '6px' );
} else {
$extra = $( '<input>' ).attr( 'type', 'text' ).attr( 'placeholder', label ).css( 'width', '100%' ).css( 'margin-bottom', '6px' );
plainExtraInputs.push( $extra );
}
extraInputs[ key ] = $extra;
$wrapper.append( $( '<div>' ).append( $extra ) );
} );
}
var $button = $( '<button>' ).text( buttonLabel );
var $status = $( '<span>' ).addClass( 'gar-instant-status' );
$wrapper.append( $button, ' ', $status );
$container.empty().append( $wrapper );
var requiredLetter = bucketLetterFromPrefix( prefix );
function submit() {
var name = sanitizeName( $input.val() );
if ( !name ) {
mw.notify( 'Please use a plain name without the characters / # < > [ ] { } |', { type: 'error', tag: 'gar-name' } );
return;
}
if ( requiredLetter && firstLetter( name ) !== requiredLetter ) {
$status.text( '' );
mw.notify(
'"' + name + '" does not start with the letter "' + requiredLetter +
'". Pick the correct letter, or fix the spelling.',
{ type: 'error', tag: 'gar-letter' }
);
return;
}
var fields = {};
for ( var key in extraInputs ) {
fields[ key ] = $.trim( extraInputs[ key ].val() );
}
runInstantCreate( prefix + name, preloadTitle, fields, $status );
}
$button.on( 'click', submit );
$input.on( 'keydown', function ( e ) {
if ( e.which === 13 ) {
e.preventDefault();
submit();
}
} );
plainExtraInputs.forEach( function ( $extra ) {
$extra.on( 'keydown', function ( e ) {
if ( e.which === 13 ) {
e.preventDefault();
submit();
}
} );
} );
}
/* ==========================================================
"Create case" button: auto-numbers Case-0000001, 0000002 ...
========================================================== */
function buildCreateCase( $container ) {
if ( !markDone( $container ) ) {
return;
}
var person = $container.data( 'person' ); // e.g. GAR:Aaa/A/Australia/A/Adelaide/Stevie
var preloadTitle = $container.data( 'preload' ); // Template:Preload/Case
var $button = $( '<button>' ).text( 'Create case' );
var $status = $( '<span>' ).addClass( 'gar-instant-status' );
$container.empty().append( $button, ' ', $status );
function submit() {
$button.prop( 'disabled', true );
$status.text( ' Creating case...' );
mw.loader.using( 'mediawiki.api' ).then( function () {
var api = new mw.Api();
var noNs = stripNs( String( person ).replace( /_/g, ' ' ) );
// 1. Find the highest existing case number under this person.
api.get( {
action: 'query',
list: 'allpages',
apnamespace: GAR_NS,
apprefix: noNs + '/Case-',
aplimit: 'max',
formatversion: 2,
format: 'json'
} ).then( function ( data ) {
var pages = ( data.query && data.query.allpages ) || [];
var max = 0;
pages.forEach( function ( p ) {
var m = p.title.match( /\/Case-(\d+)$/ );
if ( m ) {
var num = parseInt( m[ 1 ], 10 );
if ( num > max ) {
max = num;
}
}
} );
var caseName = 'Case-' + padCase( max + 1 );
var newTitle = person + '/' + caseName;
// 2. Read the case skeleton, unwrap it, create the page.
api.get( {
action: 'query',
titles: preloadTitle,
prop: 'revisions',
rvprop: 'content',
rvslots: 'main',
formatversion: 2,
format: 'json'
} ).then( function ( tpl ) {
var tp = ( tpl.query && tpl.query.pages ) || [];
var raw = '';
tp.forEach( function ( p ) {
if ( p.revisions && p.revisions[ 0 ] && p.revisions[ 0 ].slots && p.revisions[ 0 ].slots.main ) {
raw = p.revisions[ 0 ].slots.main.content;
}
} );
if ( !raw ) {
$status.text( ' Could not read the case template.' );
$button.prop( 'disabled', false );
return;
}
api.postWithToken( 'csrf', {
action: 'edit',
title: newTitle,
text: stripPreload( raw ),
createonly: true,
formatversion: 2,
summary: 'Auto-created case'
} ).then( function () {
// 3. Reflect it on the person's page: reload so the new
// case link appears in the list above.
window.location.reload();
}, function ( code ) {
if ( code === 'articleexists' ) {
window.location.reload();
return;
}
$status.text( ' Could not create the case (' + code + ').' );
$button.prop( 'disabled', false );
} );
}, function ( code ) {
$status.text( ' Could not read the case template (' + code + ').' );
$button.prop( 'disabled', false );
} );
}, function ( code ) {
$status.text( ' Could not check case numbers (' + code + ').' );
$button.prop( 'disabled', false );
} );
} );
}
$button.on( 'click', submit );
}
/* ==========================================================
Set the person's photo (shown on the person's own page).
Rewrites {{PersonPhoto|...}} on the current page.
========================================================== */
function readCurrentPage( api ) {
var pageTitle = mw.config.get( 'wgPageName' );
return api.get( {
action: 'query',
titles: pageTitle,
prop: 'revisions',
rvprop: 'content',
rvslots: 'main',
formatversion: 2,
format: 'json'
} ).then( function ( data ) {
var pages = ( data.query && data.query.pages ) || [];
var raw = '';
pages.forEach( function ( p ) {
if ( p.revisions && p.revisions[ 0 ] && p.revisions[ 0 ].slots && p.revisions[ 0 ].slots.main ) {
raw = p.revisions[ 0 ].slots.main.content;
}
} );
return { title: pageTitle, text: raw };
} );
}
// Shared: upload a chosen file, then hand the final filename to onName.
// Handles the "already uploaded" case by reusing the existing name.
function uploadThen( file, $status, onName, reEnable ) {
$status.text( ' Uploading...' );
mw.loader.using( 'mediawiki.api' ).then( function () {
var api = new mw.Api();
api.upload( file, {
filename: file.name,
ignorewarnings: true,
comment: 'Upload for ' + mw.config.get( 'wgPageName' )
} ).then( function ( result ) {
var fn = file.name;
if ( result && result.upload && result.upload.filename ) {
fn = result.upload.filename;
}
$status.text( ' Uploaded. Adding it to the page...' );
onName( api, fn, $status, reEnable );
}, function ( code ) {
// The exact image already exists -> just use that name.
if ( code === 'fileexists-no-change' || code === 'duplicate' || code === 'exists' ) {
onName( new mw.Api(), file.name, $status, reEnable );
return;
}
$status.text( ' Upload failed (' + code + '). Choose a file and try again.' );
if ( reEnable ) { reEnable(); }
} );
}, function () {
$status.text( ' Could not start the upload. Try again.' );
if ( reEnable ) { reEnable(); }
} );
}
// Pre-upload checks. Keep these two numbers equal to LocalSettings:
// $wgMaxUploadSize (5 MB) and $wgMaxImageArea (1.25e7 = 12.5 megapixels).
var GAR_MAX_FILE_MB = 5;
var GAR_MAX_MEGAPIXELS = 12.5;
function checkImageOk( file, cb ) {
if ( file.size > GAR_MAX_FILE_MB * 1024 * 1024 ) {
cb( 'This file is ' + ( file.size / 1048576 ).toFixed( 1 ) + ' MB; the maximum is ' +
GAR_MAX_FILE_MB + ' MB. Compress it and try again.' );
return;
}
var url = URL.createObjectURL( file );
var img = new Image();
img.onload = function () {
var mp = ( img.naturalWidth * img.naturalHeight ) / 1e6;
URL.revokeObjectURL( url );
if ( mp > GAR_MAX_MEGAPIXELS ) {
cb( 'This image is ' + img.naturalWidth + '\u00d7' + img.naturalHeight + ' = ' +
mp.toFixed( 1 ) + ' megapixels; the maximum is ' + GAR_MAX_MEGAPIXELS +
'. Resize it smaller and try again.' );
} else {
cb( null );
}
};
img.onerror = function () {
URL.revokeObjectURL( url );
cb( 'This does not look like a readable image file.' );
};
img.src = url;
}
// Small reusable file-picker + button. onFile(file) runs on click.
function makeUploader( buttonLabel, onFile ) {
var $file = $( '<input>' ).attr( 'type', 'file' )
.attr( 'accept', 'image/png,image/jpeg,image/webp' )
.css( 'margin-bottom', '6px' );
var $btn = $( '<button>' ).text( buttonLabel );
var $status = $( '<span>' ).addClass( 'gar-instant-status' );
var $wrap = $( '<div>' ).append(
$( '<div>' ).append( $file ),
$( '<div>' ).append( $btn, ' ', $status )
);
$btn.on( 'click', function () {
var file = $file[ 0 ].files && $file[ 0 ].files[ 0 ];
if ( !file ) {
mw.notify( 'Choose an image file first.', { type: 'error', tag: 'gar-upl' } );
return;
}
$btn.prop( 'disabled', true );
$status.text( ' Checking the image...' );
checkImageOk( file, function ( err ) {
if ( err ) {
$status.text( ' ' + err );
$btn.prop( 'disabled', false );
return;
}
onFile( file, $status, function () {
$btn.prop( 'disabled', false );
} );
} );
} );
return $wrap;
}
// Person page: upload -> set {{PersonPhoto|filename}}, then reload.
function setPhotoOnPage( api, filename, $status, reEnable ) {
return readCurrentPage( api ).then( function ( page ) {
if ( !page.text ) {
$status.text( ' Could not read this page.' );
if ( reEnable ) { reEnable(); }
return;
}
var replaced = page.text.replace(
/\{\{PersonPhoto\s*\|[^}]*\}\}/,
'{{PersonPhoto|' + escapeWikitext( filename ) + '}}'
);
if ( replaced === page.text ) {
$status.text( ' This page has no photo slot — recreate it from the current template.' );
if ( reEnable ) { reEnable(); }
return;
}
return api.postWithToken( 'csrf', {
action: 'edit',
title: page.title,
text: replaced,
formatversion: 2,
summary: 'Set photo'
} ).then( function () {
window.location.reload();
}, function ( code ) {
$status.text( ' Save failed (' + code + '). Try again.' );
if ( reEnable ) { reEnable(); }
} );
}, function () {
$status.text( ' Could not read this page. Try again.' );
if ( reEnable ) { reEnable(); }
} );
}
// Case page: upload -> append [[File:filename|400px]] to the Evidence
// area (before the end marker), then reload. Multiple images stack.
function appendEvidence( api, filename, $status, reEnable ) {
var END = '<!--GAR-EVIDENCE-END-->';
return readCurrentPage( api ).then( function ( page ) {
if ( !page.text ) {
$status.text( ' Could not read this page.' );
if ( reEnable ) { reEnable(); }
return;
}
if ( page.text.indexOf( END ) === -1 ) {
$status.text( ' This page has no evidence slot — recreate it from the current template.' );
if ( reEnable ) { reEnable(); }
return;
}
var line = '[[File:' + escapeWikitext( filename ) + '|400px]]\n';
var newText = page.text.replace( END, line + END );
return api.postWithToken( 'csrf', {
action: 'edit',
title: page.title,
text: newText,
formatversion: 2,
summary: 'Add evidence'
} ).then( function () {
window.location.reload();
}, function ( code ) {
$status.text( ' Save failed (' + code + '). Try again.' );
if ( reEnable ) { reEnable(); }
} );
}, function () {
$status.text( ' Could not read this page. Try again.' );
if ( reEnable ) { reEnable(); }
} );
}
function buildSetPhoto( $container ) {
if ( !markDone( $container ) ) {
return;
}
$container.empty().append(
makeUploader( 'Upload photo & show it here', function ( file, $status, reEnable ) {
uploadThen( file, $status, setPhotoOnPage, reEnable );
} )
);
}
function buildAddImage( $container ) {
if ( !markDone( $container ) ) {
return;
}
$container.empty().append(
makeUploader( 'Upload evidence & add it here', function ( file, $status, reEnable ) {
uploadThen( file, $status, appendEvidence, reEnable );
} )
);
}
/* ==========================================================
Editable "details" text box on the person page.
Rewrites the text between the DESC markers.
========================================================== */
function buildEditable( $container ) {
if ( !markDone( $container ) ) {
return;
}
var START = '<!--GAR-DESC-START-->';
var END = '<!--GAR-DESC-END-->';
var $textarea = $( '<textarea>' ).attr( 'rows', 4 ).css( 'width', '100%' ).css( 'margin-bottom', '6px' ).attr( 'placeholder', 'Add or update details about this person...' );
var $button = $( '<button>' ).text( 'Save details' );
var $status = $( '<span>' ).addClass( 'gar-instant-status' );
$container.empty().append(
$( '<div>' ).append( $textarea ),
$( '<div>' ).append( $button, ' ', $status )
);
mw.loader.using( 'mediawiki.api' ).then( function () {
var api = new mw.Api();
readCurrentPage( api ).then( function ( page ) {
var s = page.text.indexOf( START );
var e = page.text.indexOf( END );
if ( s > -1 && e > -1 ) {
$textarea.val( page.text.substring( s + START.length, e ) );
}
} );
function submit() {
var val = $textarea.val();
$status.text( ' Saving...' );
readCurrentPage( api ).then( function ( page ) {
var s = page.text.indexOf( START );
var e = page.text.indexOf( END );
if ( s < 0 || e < 0 ) {
$status.text( ' Could not find the details area on this page.' );
return;
}
var newRaw = page.text.substring( 0, s + START.length ) + escapeWikitext( val ) + page.text.substring( e );
api.postWithToken( 'csrf', {
action: 'edit',
title: page.title,
text: newRaw,
formatversion: 2,
summary: 'Update details'
} ).then( function () {
window.location.reload();
}, function ( code ) {
$status.text( ' Save failed (' + code + ').' );
} );
} );
}
$button.on( 'click', submit );
} );
}
/* ==========================================================
A-Z letter grid. Batch-checks which letters already exist,
turns those into ordinary links (no "Creating"), and leaves
only genuinely-new letters as clickable "add" links.
========================================================== */
function createLetter( $el ) {
var title = $el.data( 'title' );
var preload = $el.data( 'preload' );
var $status = $( '<span>' ).addClass( 'gar-instant-status' );
$el.after( $status );
$el.off( 'click' );
$el.css( 'cursor', 'default' );
runInstantCreate( title, preload, {}, $status );
}
function processLetterGrid( $content ) {
var $letters = $content.find( '.gar-instant-letter' ).filter( function () {
return !$( this ).data( 'garDone' );
} );
if ( !$letters.length ) {
return;
}
$letters.each( function () {
$( this ).data( 'garDone', true );
} );
var titles = [];
$letters.each( function () {
titles.push( String( $( this ).data( 'title' ) ) );
} );
var uniq = titles.filter( function ( t, i ) {
return titles.indexOf( t ) === i;
} );
mw.loader.using( 'mediawiki.api' ).then( function () {
var api = new mw.Api();
var existsMap = {};
var chunks = [];
var i;
for ( i = 0; i < uniq.length; i += 50 ) {
chunks.push( uniq.slice( i, i + 50 ) );
}
var seq = Promise.resolve();
chunks.forEach( function ( chunk ) {
seq = seq.then( function () {
return api.get( {
action: 'query',
titles: chunk.join( '|' ),
formatversion: 2,
format: 'json'
} ).then( function ( data ) {
var pages = ( data.query && data.query.pages ) || [];
pages.forEach( function ( p ) {
existsMap[ p.title.replace( /_/g, ' ' ) ] = !p.missing;
} );
} );
} );
} );
seq.then( function () {
$letters.each( function () {
var $el = $( this );
var title = String( $el.data( 'title' ) ).replace( /_/g, ' ' );
if ( existsMap[ title ] ) {
var $link = $( '<a>' )
.attr( 'href', mw.util.getUrl( $el.data( 'title' ) ) )
.addClass( 'gar-letter-exists' )
.text( $el.text() );
$el.replaceWith( $link );
} else {
$el.addClass( 'gar-letter-new' );
$el.on( 'click', function () {
createLetter( $el );
} );
}
} );
} );
} );
}
/* ==========================================================
Wire everything up
========================================================== */
mw.hook( 'wikipage.content' ).add( function ( $content ) {
$content.find( '.gar-auto-grid' ).each( function () {
renderAutoGrid( $( this ) );
} );
$content.find( '.gar-instant-create' ).each( function () {
buildForm( $( this ) );
} );
$content.find( '.gar-create-case' ).each( function () {
buildCreateCase( $( this ) );
} );
$content.find( '.gar-set-photo' ).each( function () {
buildSetPhoto( $( this ) );
} );
$content.find( '.gar-add-image' ).each( function () {
buildAddImage( $( this ) );
} );
$content.find( '.gar-editable' ).each( function () {
buildEditable( $( this ) );
} );
processLetterGrid( $content );
} );
}() );