Jump to content

MediaWiki:Gadget-EditParam.js

From Appropedia

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.
// <nowiki>
const EditParam = {

	// @todo i18n
	messages: {
		'editparam-template-not-found': 'Template "$1" not found in this page',
		'editparam-template-repeated': 'Template "$1" was found more than once in this page',
		'editparam-title': 'Edit parameter',
		'editparam-example': 'Example: $1',
		'editparam-publish': 'Publish',
		'editparam-cancel': 'Cancel',
		'editparam-summary': 'Edit summary',
		'editparam-summary-description': 'Briefly describe what you changed and why',
		'editparam-summary-insert': 'Add parameter "$1" with value "$2" to Template:$3',
		'editparam-summary-update': 'Change parameter "$1" from "$2" to "$3" in Template:$4',
		'editparam-summary-delete': 'Delete parameter "$1" from Template:$2',
	},

	/**
	 * Will hold the wikitext of the current page
	 */
	pageWikitext: '',

	/**
	 * Will hold the template data of the template(s) being edited
	 */
	templateData: {},

	init: function ( $content ) {
		$content.find( '.EditParam' ).each( EditParam.makeEditButton );
	},

	makeEditButton: function () {
		const button = this;
		const pencil = '<path fill="currentColor" d="M16.77 8l1.94-2a1 1 0 0 0 0-1.41l-3.34-3.3a1 1 0 0 0-1.41 0L12 3.23zm-5.81-3.71L1 14.25V19h4.75l9.96-9.96-4.75-4.75z"></path>';
		const icon = '<svg width="14" height="14" viewBox="0 0 20 20">' + pencil + '</svg>';
		button.innerHTML = icon;
		button.classList.add( 'noprint' );
		button.style.cursor = 'pointer';
		button.style.color = '#a2a9b1';
		button.onmouseenter = () => button.style.color = '#202122';
		button.onmouseleave = () => button.style.color = '#a2a9b1';
		button.onclick = EditParam.edit;
	},

	edit: async function () {
		if ( !EditParam.pageWikitext ) {
			EditParam.pageWikitext = await EditParam.getPageWikitext();
		}

		mw.messages.set( EditParam.messages );

		// Check that the template is present in the page
		// @todo Support nested templates
		const button = this;
		const template = button.dataset.template;
		const templateRegExp = new RegExp( '{{' + template + '[^}]*?}}', 'ig' );
		const match = EditParam.pageWikitext.match( templateRegExp );
		if ( !match ) {
			mw.notify( mw.msg( 'editparam-template-not-found', template ) );
			return;
		}

		// Check that there's only one template in the page
		// @todo Support multiple templates
		if ( match.length > 1 ) {
			mw.notify( mw.msg( 'editparam-template-repeated', template ) );
			return;
		}

		if ( !EditParam.templateData[ template ] ) {
			EditParam.templateData[ template ] = await EditParam.getTemplateData( template );
		}

		EditParam.makeEditForm( button, template );
	},

	makeEditForm: async function ( button, template ) {
		const require = await mw.loader.using( '@wikimedia/codex' );
		const Vue = require( 'vue' );
		const {
			CdxDialog,
			CdxField,
			CdxTextInput,
			CdxCombobox,
			CdxChipInput,
		} = require( '@wikimedia/codex' );

		const app = Vue.createMwApp( {
			setup: function () {
				const showDialog = Vue.ref( true );

				const dialogTitle = mw.msg( 'editparam-title' );

				const userLanguage = mw.config.get( 'wgUserLanguage' );
				const contentLanguage = mw.config.get( 'wgContentLanguage' );
				const templateData = EditParam.templateData[ template ];
				const paramName = button.dataset.param;
				const paramData = templateData.params && templateData.params[ paramName ] || {};
				const paramType = paramData.type || 'string';
				const paramLabel = paramData.label && ( paramData.label[ userLanguage ] || paramData.label[ contentLanguage ] ) || paramName;
				const paramDescription = paramData.description && ( paramData.description[ userLanguage ] || paramData.description[ contentLanguage ] );
				const paramValue = button.dataset.value || '';
				const paramInput = Vue.ref( paramValue );
				const paramExample = paramData.example && ( paramData.example[ userLanguage ] || paramData.example[ contentLanguage ] );
				const paramHelp = paramExample && mw.msg( 'editparam-example', paramExample );
				const paramSeparator = button.dataset.separator;

				let paramSuggestedValues;
				if ( paramData.suggestedvalues && paramData.suggestedvalues.length ) {
					paramSuggestedValues = [];
					for ( const value of paramData.suggestedvalues ) {
						paramSuggestedValues.push( { value: value } );
					}
				}

				let paramChips;
				if ( paramSeparator ) {
					paramChips = [];
					const paramValues = paramValue.split( paramSeparator );
					for ( const value of paramValues ) {
						paramChips.push( { value: value.trim() } );
					}
					paramChips = Vue.ref( paramChips );
				}

				const summaryLabel = mw.msg( 'editparam-summary' );
				const summaryInput = Vue.ref( '' );
				const summaryDescription = mw.msg( 'editparam-summary-description' );

				const primaryAction = {
					label: mw.msg( 'editparam-publish' ),
					actionType: 'progressive'
				};
				const defaultAction = {
					label: mw.msg( 'editparam-cancel' )
				};
				function onPrimaryAction() {
					const oldValue = paramValue;
					const newValue = paramChips ? paramChips.value.map( chip => chip.value ).join( paramSeparator + ' ' ) : paramInput.value;
					const summary = summaryInput.value;
					EditParam.publish( button, template, paramName, paramType, oldValue, newValue, summary );
					showDialog.value = false;
				}
				function onDefaultAction() {
					showDialog.value = false;
				}

				return {
					showDialog,
					defaultAction,
					primaryAction,
					onPrimaryAction,
					onDefaultAction,
					dialogTitle,
					paramLabel,
					paramDescription,
					paramInput,
					paramSuggestedValues,
					paramHelp,
					paramChips,
					summaryLabel,
					summaryInput,
					summaryDescription,
				};
			},
			components: {
				CdxDialog,
				CdxField,
				CdxTextInput,
				CdxCombobox,
				CdxChipInput
			},
			template: `
				<cdx-dialog
					v-model:open="showDialog"
					:title="dialogTitle"
					:use-close-button="true"
					:primary-action="primaryAction"
					:default-action="defaultAction"
					@primary="onPrimaryAction"
					@default="onDefaultAction"
				>
					<cdx-field :autofocus="false">
						<template #label>{{ paramLabel }}</template>
						<template #description>{{ paramDescription }}</template>
						<cdx-chip-input v-if="paramChips" v-model:input-chips="paramChips"></cdx-chip-input>
						<cdx-combobox v-else-if="paramSuggestedValues" :menu-items="paramSuggestedValues" v-model:selected="paramInput"></cdx-combobox>
						<cdx-text-input v-else v-model="paramInput"></cdx-text-input>
						<template #help-text>{{ paramHelp }}</template>
					</cdx-field>
					<cdx-field :optional="true">
						<template #label>{{ summaryLabel }}</template>
						<template #description>{{ summaryDescription }}</template>
						<cdx-text-input v-model="summaryInput"></cdx-text-input>
					</cdx-field>
				</cdx-dialog>
			`
		} );

		// Create a dummy element to mount the app
		const mountPoint = document.createElement( 'div' );
		document.body.append( mountPoint );
		app.mount( mountPoint );
	},

	publish: async function ( button, template, paramName, paramType, oldValue, newValue, summary ) {

		// If nothing changed, simply close the dialog
		if ( newValue === oldValue ) {
			return;
		}

		// First replace the button for a loading spinner
		// to prevent further clicks and to signal the user that something's happening
		const spinner = EditParam.makeSpinner();
		button.replaceChildren( spinner );

		const page = mw.config.get( 'wgPageName' );
		await new mw.Api().edit( page, revision => {
	        let wikitext = revision.content;

			// @todo Support nested templates
			const templateRegExp = new RegExp( '{{' + template + '[^}]*?}}', 'i' );
			const templateMatch = wikitext.match( templateRegExp );

			// This should never happen because we already checked
			// but maybe someone removed the template or something
			if ( !templateMatch ) {
				mw.notify( mw.msg( 'editparam-template-not-found', template ) );
				return;
			}

			// @todo Support inline format
			const templateWikitext = templateMatch[0];
			const paramRegExp = new RegExp( '\\n\\| *' + paramName + ' *= *.*' );
			const paramMatch = templateWikitext.match( paramRegExp );
			let newTemplateWikitext;
			if ( paramMatch ) {
				if ( newValue ) {
					newTemplateWikitext = templateWikitext.replace( paramRegExp, '\n| ' + paramName + ' = ' + newValue );
				} else {
					newTemplateWikitext = templateWikitext.replace( paramRegExp, '' );
				}
			} else {
				newTemplateWikitext = templateWikitext.replace( '}}', '\n| ' + paramName + ' = ' + newValue + '\n}}' );
			}
			wikitext = wikitext.replace( templateWikitext, newTemplateWikitext );

			summary = EditParam.makeSummary( summary, template, paramName, oldValue, newValue );

			return { text: wikitext, summary: summary };
	    } );

		// Reload the entire page to show the changes
		// The ideal UX would be to simply replace the value of the parameter
		// but that is actually very complicated (even impossible) to do well.
		// The "second best" solution would be to replace the entire template,
		// this will be doable once Parsoid is enabled by default in all page views.
		// In the meantime, modern browsers remember the current scroll position,
		// so simply reloading the page is a "good enough" solution.
		window.location.reload();
	},

	/**
	 * Get the wikitext of the current page
	 */
	getPageWikitext: async function () {
		const data = await new mw.Api().get( {
			page: mw.config.get( 'wgPageName' ),
			action: 'parse',
			prop: 'wikitext',
			formatversion: 2,
		} );
		const pageWikitext = data.parse.wikitext;
		return pageWikitext;
	},

	/**
	 * Get the template data of the given template
	 */
	getTemplateData: async function ( template ) {
		const data = await new mw.Api().get( {
			titles: 'Template:' + template,
			action: 'templatedata',
			redirects: true,
			includeMissingTitles: true,
			formatversion: 2
		} );
		const templateData = Object.values( data.pages )[0];
		return templateData;
	},

	/**
	 * Helper method to make a helpful edit summary
	 */
	 makeSummary: function ( summary, template, paramName, oldValue, newValue ) {
		if ( !summary ) {
			if ( newValue ) {
				if ( oldValue ) {
					summary = mw.msg( 'editparam-summary-update', paramName, oldValue, newValue, template );
				} else {
					summary = mw.msg( 'editparam-summary-insert', paramName, newValue, template );
				}
			} else {
				summary = mw.msg( 'editparam-summary-delete', paramName, template );
			}
		}
		summary += ' #EditParam';
		return summary;
	 },

	/**
	 * Helper method to make a spinner (loading) icon
	 */
	 makeSpinner: function () {
		let spinner = '<svg width="14" height="14" viewBox="0 0 100 100">';
		spinner += '<rect fill="#555555" height="10" rx="5" ry="5" width="28" x="67" y="45" opacity="0.000" transform="rotate(-90 50 50)" />';
		spinner += '<rect fill="#555555" height="10" rx="5" ry="5" width="28" x="67" y="45" opacity="0.125" transform="rotate(-45 50 50)" />';
		spinner += '<rect fill="#555555" height="10" rx="5" ry="5" width="28" x="67" y="45" opacity="0.250" transform="rotate(0 50 50)" />';
		spinner += '<rect fill="#555555" height="10" rx="5" ry="5" width="28" x="67" y="45" opacity="0.375" transform="rotate(45 50 50)" />';
		spinner += '<rect fill="#555555" height="10" rx="5" ry="5" width="28" x="67" y="45" opacity="0.500" transform="rotate(90 50 50)" />';
		spinner += '<rect fill="#555555" height="10" rx="5" ry="5" width="28" x="67" y="45" opacity="0.625" transform="rotate(135 50 50)" />';
		spinner += '<rect fill="#555555" height="10" rx="5" ry="5" width="28" x="67" y="45" opacity="0.750" transform="rotate(180 50 50)" />';
		spinner += '<rect fill="#555555" height="10" rx="5" ry="5" width="28" x="67" y="45" opacity="0.875" transform="rotate(225 50 50)" />';
		spinner += '</svg>';
		spinner = $( spinner );
		spinner = spinner[0];
		let degrees = 0;
		setInterval( () => {
			degrees += 45;
			spinner.style.transform = 'rotate(' + degrees + 'deg)';
		}, 100 );
		return spinner;
	}
};

mw.hook( 'wikipage.content' ).add( EditParam.init );
// </nowiki>
Cookies help us deliver our services. By using our services, you agree to our use of cookies.