Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

MediaWiki:Common.js

MediaWiki interface page
Revision as of 17:48, 5 July 2026 by Palindromayd (talk | contribs)

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';

	/* ---------- 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, '&#123;' )
			.replace( /\}/g, '&#125;' )
			.replace( /\[/g, '&#91;' )
			.replace( /\]/g, '&#93;' )
			.replace( /\|/g, '&#124;' );
	}

	// 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 ) );
		} );
	} );
}() );