MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
Palindromayd (talk | contribs) No edit summary |
Palindromayd (talk | contribs) No edit summary |
||
| Line 502: | Line 502: | ||
// Shared: upload a chosen file, then hand the final filename to onName. | // Shared: upload a chosen file, then hand the final filename to onName. | ||
// Handles the "already uploaded" case by reusing the existing name. | // Handles the "already uploaded" case by reusing the existing name. | ||
function uploadThen( file, $status, onName ) { | function uploadThen( file, $status, onName, reEnable ) { | ||
$status.text( ' Uploading...' ); | $status.text( ' Uploading...' ); | ||
mw.loader.using( 'mediawiki.api' ).then( function () { | mw.loader.using( 'mediawiki.api' ).then( function () { | ||
| Line 516: | Line 516: | ||
} | } | ||
$status.text( ' Uploaded. Adding it to the page...' ); | $status.text( ' Uploaded. Adding it to the page...' ); | ||
onName( api, fn, $status ); | onName( api, fn, $status, reEnable ); | ||
}, function ( code ) { | }, function ( code ) { | ||
// The exact image already exists -> just use that name. | // The exact image already exists -> just use that name. | ||
if ( code === 'fileexists-no-change' || code === 'duplicate' || code === 'exists' ) { | if ( code === 'fileexists-no-change' || code === 'duplicate' || code === 'exists' ) { | ||
onName( new mw.Api(), file.name, $status ); | onName( new mw.Api(), file.name, $status, reEnable ); | ||
return; | return; | ||
} | } | ||
$status.text( ' Upload failed (' + code + ').' ); | $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(); } | |||
} ); | } ); | ||
} | } | ||
| Line 554: | Line 558: | ||
// Person page: upload -> set {{PersonPhoto|filename}}, then reload. | // Person page: upload -> set {{PersonPhoto|filename}}, then reload. | ||
function setPhotoOnPage( api, filename, $status ) { | function setPhotoOnPage( api, filename, $status, reEnable ) { | ||
return readCurrentPage( api ).then( function ( page ) { | return readCurrentPage( api ).then( function ( page ) { | ||
if ( !page.text ) { | if ( !page.text ) { | ||
$status.text( ' Could not read this page.' ); | $status.text( ' Could not read this page.' ); | ||
if ( reEnable ) { reEnable(); } | |||
return; | return; | ||
} | } | ||
| Line 566: | Line 571: | ||
if ( replaced === page.text ) { | if ( replaced === page.text ) { | ||
$status.text( ' This page has no photo slot — recreate it from the current template.' ); | $status.text( ' This page has no photo slot — recreate it from the current template.' ); | ||
if ( reEnable ) { reEnable(); } | |||
return; | return; | ||
} | } | ||
| Line 577: | Line 583: | ||
window.location.reload(); | window.location.reload(); | ||
}, function ( code ) { | }, function ( code ) { | ||
$status.text( ' Save failed (' + code + ').' ); | $status.text( ' Save failed (' + code + '). Try again.' ); | ||
if ( reEnable ) { reEnable(); } | |||
} ); | } ); | ||
}, function () { | |||
$status.text( ' Could not read this page. Try again.' ); | |||
if ( reEnable ) { reEnable(); } | |||
} ); | } ); | ||
} | } | ||
| Line 584: | Line 594: | ||
// Case page: upload -> append [[File:filename|400px]] to the Evidence | // Case page: upload -> append [[File:filename|400px]] to the Evidence | ||
// area (before the end marker), then reload. Multiple images stack. | // area (before the end marker), then reload. Multiple images stack. | ||
function appendEvidence( api, filename, $status ) { | function appendEvidence( api, filename, $status, reEnable ) { | ||
var END = '<!--GAR-EVIDENCE-END-->'; | var END = '<!--GAR-EVIDENCE-END-->'; | ||
return readCurrentPage( api ).then( function ( page ) { | return readCurrentPage( api ).then( function ( page ) { | ||
if ( !page.text ) { | if ( !page.text ) { | ||
$status.text( ' Could not read this page.' ); | $status.text( ' Could not read this page.' ); | ||
if ( reEnable ) { reEnable(); } | |||
return; | return; | ||
} | } | ||
if ( page.text.indexOf( END ) === -1 ) { | if ( page.text.indexOf( END ) === -1 ) { | ||
$status.text( ' This page has no evidence slot — recreate it from the current template.' ); | $status.text( ' This page has no evidence slot — recreate it from the current template.' ); | ||
if ( reEnable ) { reEnable(); } | |||
return; | return; | ||
} | } | ||
| Line 606: | Line 618: | ||
window.location.reload(); | window.location.reload(); | ||
}, function ( code ) { | }, function ( code ) { | ||
$status.text( ' Save failed (' + code + ').' ); | $status.text( ' Save failed (' + code + '). Try again.' ); | ||
if ( reEnable ) { reEnable(); } | |||
} ); | } ); | ||
}, function () { | |||
$status.text( ' Could not read this page. Try again.' ); | |||
if ( reEnable ) { reEnable(); } | |||
} ); | } ); | ||
} | } | ||
| Line 616: | Line 632: | ||
} | } | ||
$container.empty().append( | $container.empty().append( | ||
makeUploader( 'Upload photo & show it here', function ( file, $status ) { | makeUploader( 'Upload photo & show it here', function ( file, $status, reEnable ) { | ||
uploadThen( file, $status, setPhotoOnPage ); | uploadThen( file, $status, setPhotoOnPage, reEnable ); | ||
} ) | } ) | ||
); | ); | ||
| Line 627: | Line 643: | ||
} | } | ||
$container.empty().append( | $container.empty().append( | ||
makeUploader( 'Upload evidence & add it here', function ( file, $status ) { | makeUploader( 'Upload evidence & add it here', function ( file, $status, reEnable ) { | ||
uploadThen( file, $status, appendEvidence ); | uploadThen( file, $status, appendEvidence, reEnable ); | ||
} ) | } ) | ||
); | ); | ||
Revision as of 21:03, 9 July 2026
( 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(); }
} );
}
// 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 );
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 );
} );
}() );