MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
Palindromayd (talk | contribs) No edit summary |
Palindromayd (talk | contribs) No edit summary |
||
| Line 2: | Line 2: | ||
'use strict'; | 'use strict'; | ||
/* ---------- Live auto-updating grid | /* ---------- Live auto-updating grid ---------- */ | ||
function renderAutoGrid( $container ) { | function renderAutoGrid( $container ) { | ||
var category = $container.data( 'category' ); | var category = $container.data( 'category' ); | ||
| Line 9: | Line 8: | ||
return; | return; | ||
} | } | ||
$container.text( 'Loading...' ); | $container.text( 'Loading...' ); | ||
mw.loader.using( 'mediawiki.api' ).then( function () { | mw.loader.using( 'mediawiki.api' ).then( function () { | ||
return new mw.Api().get( { | return new mw.Api().get( { | ||
| Line 25: | Line 22: | ||
var members = ( data.query && data.query.categorymembers ) || []; | var members = ( data.query && data.query.categorymembers ) || []; | ||
$container.empty(); | $container.empty(); | ||
if ( members.length === 0 ) { | if ( members.length === 0 ) { | ||
$container.append( $( '<p>' ).text( 'Nothing has been added here yet.' ) ); | $container.append( $( '<p>' ).text( 'Nothing has been added here yet.' ) ); | ||
return; | return; | ||
} | } | ||
members.forEach( function ( member ) { | members.forEach( function ( member ) { | ||
var title = member.title; | var title = member.title; | ||
| Line 44: | Line 39: | ||
} | } | ||
/* ---------- | /* ---------- Safety helpers ---------- */ | ||
// Neutralise wiki characters so a typed value can't inject templates/links. | |||
function escapeWikitext( str ) { | |||
return String( str ) | |||
.replace( /\{/g, '{' ) | |||
.replace( /\}/g, '}' ) | |||
.replace( /\[/g, '[' ) | |||
.replace( /\]/g, ']' ) | |||
.replace( /\|/g, '|' ); | |||
} | |||
// Escape $ so it is safe inside a JS String.replace() replacement. | |||
function safeReplacement( str ) { | |||
return str.replace( /\$/g, '$$$$' ); | |||
} | |||
// 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(); | |||
} | |||
/* ---------- Instant-create core ---------- */ | |||
function fallbackToManualEditor( newTitle, preloadTitle, $status, reason ) { | function fallbackToManualEditor( newTitle, preloadTitle, $status, reason ) { | ||
console.error( 'GAR instant-create fell back to manual editor. Reason: ' + reason + '. Title: ' + newTitle ); | console.error( 'GAR instant-create fell back to manual editor. Reason: ' + reason + '. Title: ' + newTitle ); | ||
$status.text( ' Opening the normal editor instead...' ); | $status.text( ' Opening the normal editor instead...' ); | ||
window.location.href = mw.util.getUrl( newTitle, { action: 'edit', preload: preloadTitle } ); | window.location.href = mw.util.getUrl( newTitle, { action: 'edit', preload: preloadTitle } ); | ||
} | } | ||
function applyExtraFields( rawText, fields ) { | function applyExtraFields( rawText, fields ) { | ||
var text = rawText; | var text = rawText; | ||
if ( fields.image ) { | if ( fields.image ) { | ||
text = text.replace( /\|image(\s*)=\s*\n/, function ( match, spacing ) { | text = text.replace( /\|image(\s*)=\s*\n/, function ( match, spacing ) { | ||
return '|image' + spacing + '= ' + safeReplacement( fields.image ) + '\n'; | return '|image' + spacing + '= ' + safeReplacement( escapeWikitext( fields.image ) ) + '\n'; | ||
} ); | } ); | ||
} | } | ||
if ( fields.evidence ) { | if ( fields.evidence ) { | ||
text = text.replace( '<Add Picture Here>', safeReplacement( fields.evidence ) ); | text = text.replace( '<Add Picture Here>', safeReplacement( escapeWikitext( fields.evidence ) ) ); | ||
} | } | ||
if ( fields.description ) { | if ( fields.description ) { | ||
text = text.replace( '<Write Text Here>', safeReplacement( fields.description ) ); | text = text.replace( '<Write Text Here>', safeReplacement( escapeWikitext( fields.description ) ) ); | ||
} | } | ||
return text; | return text; | ||
} | } | ||
| Line 78: | Line 93: | ||
function runInstantCreate( newTitle, preloadTitle, fields, $status ) { | function runInstantCreate( newTitle, preloadTitle, fields, $status ) { | ||
$status.text( ' Creating...' ); | $status.text( ' Creating...' ); | ||
mw.loader.using( 'mediawiki.api' ).then( function () { | mw.loader.using( 'mediawiki.api' ).then( function () { | ||
var api = new mw.Api(); | var api = new mw.Api(); | ||
api.get( { | api.get( { | ||
action: 'query', | action: 'query', | ||
| Line 95: | Line 108: | ||
} | } | ||
} ); | } ); | ||
if ( exists ) { | if ( exists ) { | ||
$status.text( ' Already exists, opening it...' ); | $status.text( ' Already exists, opening it...' ); | ||
| Line 101: | Line 113: | ||
return; | return; | ||
} | } | ||
api.get( { | api.get( { | ||
action: 'query', | action: 'query', | ||
| Line 118: | Line 129: | ||
} | } | ||
} ); | } ); | ||
if ( !rawText ) { | if ( !rawText ) { | ||
fallbackToManualEditor( newTitle, preloadTitle, $status, 'preload template came back empty' ); | fallbackToManualEditor( newTitle, preloadTitle, $status, 'preload template came back empty' ); | ||
return; | return; | ||
} | } | ||
var finalText = applyExtraFields( rawText, fields ); | var finalText = applyExtraFields( rawText, fields ); | ||
api.postWithToken( 'csrf', { | api.postWithToken( 'csrf', { | ||
action: 'edit', | action: 'edit', | ||
| Line 151: | Line 159: | ||
} | } | ||
/* ---------- Typed-name forms | /* ---------- Typed-name forms ---------- */ | ||
function buildForm( $container ) { | function buildForm( $container ) { | ||
var prefix = $container.data( 'prefix' ); | var prefix = $container.data( 'prefix' ); | ||
| Line 186: | Line 193: | ||
var $status = $( '<span>' ).addClass( 'gar-instant-status' ); | var $status = $( '<span>' ).addClass( 'gar-instant-status' ); | ||
$wrapper.append( $button, ' ', $status ); | $wrapper.append( $button, ' ', $status ); | ||
$container.empty().append( $wrapper ); | |||
var requiredLetter = bucketLetterFromPrefix( prefix ); | |||
function submit() { | function submit() { | ||
| Line 194: | Line 202: | ||
return; | return; | ||
} | } | ||
// Letter check: only fires when this form sits under an A-Z letter. | |||
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 = {}; | var fields = {}; | ||
for ( var key in extraInputs ) { | for ( var key in extraInputs ) { | ||
| Line 202: | Line 222: | ||
$button.on( 'click', submit ); | $button.on( 'click', submit ); | ||
$input.on( 'keydown', function ( e ) { | $input.on( 'keydown', function ( e ) { | ||
if ( e.which === 13 ) { | if ( e.which === 13 ) { | ||
| Line 220: | Line 238: | ||
} | } | ||
/* ---------- Letter-grid clicks | /* ---------- Letter-grid clicks ---------- */ | ||
function bindInstantLetter( $el ) { | function bindInstantLetter( $el ) { | ||
$el.on( 'click', function () { | $el.on( 'click', function () { | ||
| Line 234: | Line 251: | ||
} | } | ||
/* ---------- Wire everything up | /* ---------- Wire everything up ---------- */ | ||
mw.hook( 'wikipage.content' ).add( function ( $content ) { | mw.hook( 'wikipage.content' ).add( function ( $content ) { | ||
$content.find( '.gar-auto-grid' ).each( function () { | $content.find( '.gar-auto-grid' ).each( function () { | ||
Revision as of 17:48, 5 July 2026
( function () {
'use strict';
/* ---------- Live auto-updating grid ---------- */
function renderAutoGrid( $container ) {
var category = $container.data( 'category' );
if ( !category ) {
return;
}
$container.text( 'Loading...' );
mw.loader.using( 'mediawiki.api' ).then( function () {
return new mw.Api().get( {
action: 'query',
list: 'categorymembers',
cmtitle: 'Category:' + category,
cmlimit: 500,
cmsort: 'sortkey',
formatversion: 2,
format: 'json'
} );
} ).then( function ( data ) {
var members = ( data.query && data.query.categorymembers ) || [];
$container.empty();
if ( members.length === 0 ) {
$container.append( $( '<p>' ).text( 'Nothing has been added here yet.' ) );
return;
}
members.forEach( function ( member ) {
var title = member.title;
var displayName = title.split( ':' ).slice( 1 ).join( ':' ).split( '/' ).pop();
var $link = $( '<a>' )
.attr( 'href', mw.util.getUrl( title ) )
.text( displayName );
$container.append( $link );
} );
}, function () {
$container.text( 'Could not load this list right now.' );
} );
}
/* ---------- Safety helpers ---------- */
// Neutralise wiki characters so a typed value can't inject templates/links.
function escapeWikitext( str ) {
return String( str )
.replace( /\{/g, '{' )
.replace( /\}/g, '}' )
.replace( /\[/g, '[' )
.replace( /\]/g, ']' )
.replace( /\|/g, '|' );
}
// Escape $ so it is safe inside a JS String.replace() replacement.
function safeReplacement( str ) {
return str.replace( /\$/g, '$$$$' );
}
// 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();
}
/* ---------- 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( 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, err ) {
fallbackToManualEditor( newTitle, preloadTitle, $status, 'edit call failed: ' + code );
} );
}, function ( code, err ) {
fallbackToManualEditor( newTitle, preloadTitle, $status, 'template fetch failed: ' + code );
} );
}, function ( code, err ) {
fallbackToManualEditor( newTitle, preloadTitle, $status, 'existence check failed: ' + code );
} );
} );
}
/* ---------- Typed-name forms ---------- */
function buildForm( $container ) {
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 = $.trim( $input.val() );
if ( !name ) {
return;
}
// Letter check: only fires when this form sits under an A-Z letter.
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();
}
} );
} );
}
/* ---------- Letter-grid clicks ---------- */
function bindInstantLetter( $el ) {
$el.on( 'click', function () {
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 );
} );
}
/* ---------- 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-instant-letter' ).each( function () {
bindInstantLetter( $( this ) );
} );
} );
}() );