MediaWiki:Gadget-EditParam.js
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>