Merge "Implement edit interface for TemplateData documentation"

This commit is contained in:
jenkins-bot 2014-01-15 02:41:16 +00:00 committed by Gerrit Code Review
commit b82082c46c
8 changed files with 1414 additions and 3 deletions

View file

@ -27,6 +27,7 @@
"predef": [
"mediaWiki",
"jQuery"
"jQuery",
"QUnit"
]
}

View file

@ -25,6 +25,22 @@ class TemplateDataHooks {
return true;
}
/**
* Register qunit unit tests
*/
public static function onResourceLoaderTestModules(
array &$testModules,
ResourceLoader &$resourceLoader
) {
$testModules['qunit']['ext.templateData.test'] = array(
'scripts' => array( 'tests/ext.templateData.tests.js' ),
'dependencies' => array( 'ext.templateDataGenerator.core' ),
'localBasePath' => __DIR__ ,
'remoteExtPath' => 'TemplateData',
);
return true;
}
/**
* @param Page &$page
* @param User &$user
@ -65,6 +81,23 @@ class TemplateDataHooks {
return true;
}
/**
* Parser hook registering the GUI module only in edit pages.
*
* @param EditPage $editPage
* @param OutputPage $output
* @return bool
*/
public static function onEditPage( $editPage, $output ) {
global $wgTemplateDataUseGUI;
if ( $wgTemplateDataUseGUI ) {
if ( $output->getTitle()->getNamespace() === NS_TEMPLATE ) {
$output->addModules( 'ext.templateDataGenerator.editPage' );
}
}
return true;
}
/**
* Parser hook for <templatedata>.
* If there is any JSON provided, render the template documentation on the page.

View file

@ -32,6 +32,34 @@ $messages['en'] = array(
'templatedata-invalid-unknown' => 'Unexpected property "$1".',
'templatedata-invalid-value' => 'Invalid value for property "$1".',
'templatedata-invalid-length' => 'Data too large to save ({{formatnum:$1}} {{PLURAL:$1|byte|bytes}}, {{PLURAL:$2|limit is}} {{formatnum:$2}})',
// TemplateData generator GUI:
'templatedata-editbutton' => 'Manage template documentation',
'templatedata-errormsg-jsonbadformat' => 'Bad JSON format. Either correct it, or delete the current <templatedata> tags and try again.',
'templatedata-modal-button-addparam' => 'Add parameter',
'templatedata-modal-button-apply' => 'Apply',
'templatedata-modal-button-cancel' => 'Cancel',
'templatedata-modal-button-delparam' => 'Delete parameter',
'templatedata-modal-button-importParams' => 'Import parameters',
'templatedata-modal-errormsg' => 'Errors found. Please make sure there are no empty or duplicate parameter names, and that the parameter name does not include "|", "=" or "}}".',
'templatedata-modal-errormsg-import-noparams' => 'No new parameters found during import.',
'templatedata-modal-notice-import-numparams' => '$1 new {{PLURAL:$1|parameter was|parameters were}} imported.',
'templatedata-modal-table-param-actions' => 'Actions',
'templatedata-modal-table-param-aliases' => 'Aliases (comma separated)',
'templatedata-modal-table-param-default' => 'Default',
'templatedata-modal-table-param-desc' => 'Description',
'templatedata-modal-table-param-label' => 'Label',
'templatedata-modal-table-param-name' => 'Name',
'templatedata-modal-table-param-required' => 'Required',
'templatedata-modal-table-param-type' => 'Type',
'templatedata-modal-table-param-type-number' => 'Number',
'templatedata-modal-table-param-type-page' => 'Page',
'templatedata-modal-table-param-type-string' => 'String',
'templatedata-modal-table-param-type-undefined' => 'Undefined',
'templatedata-modal-table-param-type-user' => 'User',
'templatedata-modal-title' => 'Template documentation editor',
'templatedata-modal-title-templatedesc' => 'Template description',
'templatedata-modal-title-templateparams' => 'Template parameters',
);
/** Message documentation (Message documentation)
@ -91,6 +119,32 @@ $messages['qqq'] = array(
'templatedata-invalid-length' => "Error message when generated JSON's length exceed database limits.
* $1 - length of generated JSON
* $2 - maximal allowed length",
'templatedata-editbutton' => 'The label of the button to manage templatedata, appearing above the editor field.',
'templatedata-errormsg-jsonbadformat' => 'Error message that appears in case the JSON string is not possible to parse. The user is asked to either correct the json syntax or delete the values between the &lt;templatedata&gt; tags and try again.',
'templatedata-modal-button-addparam' => 'Button to add a parameter',
'templatedata-modal-button-apply' => 'Label of the apply button',
'templatedata-modal-button-cancel' => 'Label of the cancel button',
'templatedata-modal-button-delparam' => 'Button to delete a parameter',
'templatedata-modal-button-importParams' => 'Label of the import button',
'templatedata-modal-errormsg' => 'Error message that appears in the TemplateData generator GUI in case there are empty, duplicate or invalid parameter names',
'templatedata-modal-errormsg-import-noparams' => 'message that appears in the TemplateData generator GUI in case no template parameters were found during the import attempt.',
'templatedata-modal-notice-import-numparams' => 'message that appears in the TemplateData generator GUI showing how many new parameters were imported into the GUI from an existing template.',
'templatedata-modal-table-param-actions' => 'Label for a table heading: Parameter actions in the table',
'templatedata-modal-table-param-aliases' => 'Label for a table heading: Aliases of the parameter, instruct the user to separate aliases with commas.',
'templatedata-modal-table-param-default' => 'Label for a table heading: Default value of the parameter',
'templatedata-modal-table-param-desc' => 'Label for a table heading: Description of the parameter',
'templatedata-modal-table-param-label' => 'Label for a table heading: Label of the parameter',
'templatedata-modal-table-param-name' => 'Label for a table heading: Name of the parameter',
'templatedata-modal-table-param-required' => 'Label for a table heading: Required status of the parameter',
'templatedata-modal-table-param-type' => 'Label for a table heading: Type of the parameter',
'templatedata-modal-table-param-type-number' => 'A possible parameter type: Number',
'templatedata-modal-table-param-type-page' => 'A possible parameter type: Page',
'templatedata-modal-table-param-type-string' => 'A possible parameter type: String',
'templatedata-modal-table-param-type-undefined' => 'A possible parameter type: Undefined',
'templatedata-modal-table-param-type-user' => 'A possible parameter type: User',
'templatedata-modal-title' => 'Title of the modal popup.',
'templatedata-modal-title-templatedesc' => 'The title for the template description textbox',
'templatedata-modal-title-templateparams' => 'The title for the template parameters table',
);
/** Arabic (العربية)
@ -493,12 +547,38 @@ $messages['he'] = array(
'templatedata-doc-param-desc-empty' => 'אין תיאור',
'templatedata-invalid-duplicate-value' => 'המאפיין "$1" (ערך: "$3") זהה למאפיין "$2".',
'templatedata-invalid-parse' => 'שגיאת תחביר ב־JSON.',
'templatedata-invalid-type' => 'המאפיין "$1" צפוי להיות מסוג "$2".',
'templatedata-invalid-type' => 'המאפיין "$1" אמור להיות מסוג "$2".',
'templatedata-invalid-missing' => 'המאפיין הדרוש "$1" לא נמצא.',
'templatedata-invalid-empty-array' => 'למאפיין "$1" צריך להיות לפחות ערך אחד במערך שלו.',
'templatedata-invalid-unknown' => 'מאפיין בלתי־צפוי "$1".',
'templatedata-invalid-value' => 'ערך בלתי־תקין למאפיין "$1".',
'templatedata-invalid-length' => 'הנתונים גדולים מכדי לשמור ({{formatnum:$1}} {{PLURAL:$1|בית|בתים}}, {{PLURAL:$2|המגבלה היא}} {{formatnum:$2}})',
'templatedata-invalid-length' => 'הנתונים גדולים מכדי לשמור ({{PLURAL:$1|בית אחד|{{formatnum:$1}} בתים}}, {{PLURAL:$2|המגבלה היא}} {{formatnum:$2}})',
'templatedata-editbutton' => 'יצירת נתוני תבנית',
'templatedata-errormsg-jsonbadformat' => 'JSON בלתי־תקין. נא לתקן אותו או למחוק את הטקסט בין תגי <templatedata> ולנסות שוב.',
'templatedata-modal-errormsg' => 'נמצאו שגיאות. נא לוודא ששמות הפרמטרים אינם ריקים ואינם חוזרים על עצמם, ושבשמות הפרמטרים לא מופיעים התווים |, = או }}',
'templatedata-modal-errormsg-import-noparams' => 'לא נמצאו פרמטרים חדשים בעת הייבוא',
'templatedata-modal-notice-import-numparams' => '{{PLURAL:$1|יובא פרמטר חדש אחד|יובאו $1 פרמטרים חדשים}}',
'templatedata-modal-title' => 'מחולל נתוני תבנית',
'templatedata-modal-title-templatedesc' => 'תיאור התבנית',
'templatedata-modal-title-templateparams' => 'פרמטרי תבנית',
'templatedata-modal-table-param-name' => 'שם',
'templatedata-modal-table-param-aliases' => 'כינויים (מופרדים בפסיק)',
'templatedata-modal-table-param-label' => 'תווית',
'templatedata-modal-table-param-desc' => 'תיאור',
'templatedata-modal-table-param-type' => 'סוג',
'templatedata-modal-table-param-type-undefined' => 'בלתי־מוגדר',
'templatedata-modal-table-param-type-number' => 'מספר',
'templatedata-modal-table-param-type-string' => 'מחרוזת',
'templatedata-modal-table-param-type-user' => 'משתמש',
'templatedata-modal-table-param-type-page' => 'דף',
'templatedata-modal-table-param-default' => 'ערך התחלתי',
'templatedata-modal-table-param-required' => 'נדרש',
'templatedata-modal-table-param-actions' => 'פעולות',
'templatedata-modal-button-addparam' => 'הוספת פרמטר',
'templatedata-modal-button-delparam' => 'מחיקת פרמטר',
'templatedata-modal-button-importParams' => 'ייבוא פרמטרים',
'templatedata-modal-buttons-apply' => 'החלה',
'templatedata-modal-buttons-cancel' => 'ביטול',
);
/** Upper Sorbian (hornjoserbsce)

View file

@ -34,6 +34,8 @@ $wgAutoloadClasses['ApiTemplateData'] = $dir . '/api/ApiTemplateData.php';
$wgHooks['ParserFirstCallInit'][] = 'TemplateDataHooks::onParserFirstCallInit';
$wgHooks['PageContentSave'][] = 'TemplateDataHooks::onPageContentSave';
$wgHooks['UnitTestsList'][] = 'TemplateDataHooks::onUnitTestsList';
$wgHooks['ResourceLoaderTestModules'][] = 'TemplateDataHooks::onResourceLoaderTestModules';
$wgHooks['EditPage::showEditForm:initial'][] = 'TemplateDataHooks::onEditPage';
// Register APIs
$wgAPIModules['templatedata'] = 'ApiTemplateData';
@ -48,3 +50,63 @@ $wgResourceModules['ext.templateData'] = array(
'localBasePath' => $dir,
'remoteExtPath' => 'TemplateData',
);
$wgResourceModules['ext.templateDataGenerator.editPage'] = array(
'localBasePath' => $dir,
'remoteExtPath' => 'TemplateData',
'scripts' => array(
'modules/ext.templateDataGenerator.editPage.js',
),
'dependencies' => array(
'ext.templateDataGenerator.core',
),
'messages' => array(
'templatedata-editbutton',
'templatedata-errormsg-jsonbadformat',
)
);
$wgResourceModules['ext.templateDataGenerator.core'] = array(
'localBasePath' => $dir,
'remoteExtPath' => 'TemplateData',
'styles' => 'modules/ext.templateDataGenerator.css',
'scripts' => array(
'modules/ext.templateDataGenerator.core.js',
),
'dependencies' => array(
'jquery.ui.dialog',
'jquery.ui.button',
),
'messages' => array(
'templatedata-modal-button-addparam',
'templatedata-modal-button-apply',
'templatedata-modal-button-cancel',
'templatedata-modal-button-delparam',
'templatedata-modal-button-importParams',
'templatedata-modal-errormsg',
'templatedata-modal-errormsg-import-noparams',
'templatedata-modal-notice-import-numparams',
'templatedata-modal-table-param-actions',
'templatedata-modal-table-param-aliases',
'templatedata-modal-table-param-default',
'templatedata-modal-table-param-desc',
'templatedata-modal-table-param-label',
'templatedata-modal-table-param-name',
'templatedata-modal-table-param-required',
'templatedata-modal-table-param-type',
'templatedata-modal-table-param-type-number',
'templatedata-modal-table-param-type-page',
'templatedata-modal-table-param-type-string',
'templatedata-modal-table-param-type-undefined',
'templatedata-modal-table-param-type-user',
'templatedata-modal-title',
'templatedata-modal-title-templatedesc',
'templatedata-modal-title-templateparams',
)
);
/* Configuration */
// Set this to true to use the template documentation
// editor feature
$wgTemplateDataUseGUI = false;

View file

@ -0,0 +1,836 @@
( function ( $, mw ) {
/**
* TemplateDataGenerator generates the JSON string for templatedata
* or reads existing templatedata string and allows for it to be edited
* with a visual modal GUI.
*
* @author Moriel Schottlender
*/
'use strict';
mw.libs.templateDataGenerator = ( function () {
var paramBase, paramTypes, domObjects, curr;
/* Private helper functions */
/**
* Show an error message in the main Edit screen
*
* @param {string} msg The message to display in the error box
*/
function showErrorEditPage( msg ) {
domObjects.$errorBox.text( msg ).show();
}
/**
* Helper function to clean up the aliases string-to-array
*
* @param {string} str Comma separated string
* @returns {string[]} Cleaned-up alias array
*/
function cleanupAliasArray( str ) {
return $.map( str.split( ',' ), function ( item ) {
if ( $.trim( item ).length > 0 ) {
return $.trim( item );
}
} );
}
/**
* Show an error message in the GUI
*
* @param {string} msg The message to be displayed in the error box
*/
function showErrorModal( msg ) {
domObjects.$errorModalBox.text( msg ).show();
}
/**
* Create `<select>` for parameter type based on the
* options given by key/value
*
* @param {Object} options The key/value pair for the options
* that should appear in the select input.
* @returns {jQuery} <select> object
*/
function createTypeSelect( opts ) {
var op,
$sel = $( '<select>' );
for ( op in opts ) {
$sel.append( $( '<option>' ).val( op ).text( opts[ op ] ) );
}
return $sel;
}
/**
* Parse the JSON information from the wikitext
*
* If it exists, and prepare DOM elements from
* the parameters into the global param DOM JSON.
*
* @param {string} wikitext The source of the template text
* @returns {Object} Parameters object parsed from the JSON string
*/
function parseTemplateData( wikitext ) {
var attrib,
param,
trimmedParam,
arrayParamNamesForTrimming = [],
jsonParams = {},
parts = wikitext.match(
/(<templatedata>)([\s\S]*?)(<\/templatedata>)/i
);
// Check if <templatedata> exists
if ( parts && parts[2] ) {
// Make sure it's not empty
if ( $.trim( parts[2] ).length > 0 ) {
try {
jsonParams = $.parseJSON( $.trim( parts[2] ) );
} catch ( err ) {
// Oops, JSON contains syntax error
mw.log( 'TemplateData: ' + mw.msg( 'templatedata-errormsg-jsonbadformat' ) );
if ( domObjects ) {
showErrorEditPage( mw.msg( 'templatedata-errormsg-jsonbadformat' ) );
}
return {};
}
}
// See if jsonParams has 'params'
if ( jsonParams && jsonParams.params ) {
// Add DOM elements to the JSON data params
for ( param in jsonParams.params ) {
// Trim parameter key if it contains trailing/leading whitespace
trimmedParam = $.trim( param );
// Insert into the array for later trimming
if ( trimmedParam !== param ) {
arrayParamNamesForTrimming.push( param );
}
// Only create dom params if needed
// This will allow the entire method to be called
// individually, as a tool or for qunit tests
if ( curr && curr.paramDomElements ) {
setupDomParam( trimmedParam, attrib );
}
}
}
// Trim the params we need to in the JSON object param keys
$.each( arrayParamNamesForTrimming, function ( index, paramid ) {
trimmedParam = $.trim( paramid );
jsonParams.params[trimmedParam] = jsonParams.params[paramid];
delete jsonParams.params[paramid];
} );
}
return jsonParams;
}
/**
* Create a DOM element that correspond to the parameter and field
*
* @param {String} paramName Parameter name or id
* @param {String} attrib The field that will correspond to the dom element
*/
function setupDomParam( paramName, attrib ) {
var $tmpDom;
curr.paramDomElements[paramName] = {};
// Create DOM elements per parameter
for ( attrib in paramBase ) {
// Set up the DOM element
if ( attrib === 'type' ) {
$tmpDom = createTypeSelect( paramTypes );
} else {
$tmpDom = paramBase[attrib].dom;
}
curr.paramDomElements[paramName][attrib] = $tmpDom.clone( true );
curr.paramDomElements[paramName][attrib].data( 'paramid', paramName );
curr.paramDomElements[paramName][attrib].attr( 'id', attrib + '_paramid_' + paramName );
curr.paramDomElements[paramName][attrib].addClass( 'tdg-element-attr-' + attrib );
}
// Set up the 'delete' button
curr.paramDomElements[paramName].delbutton
.text( mw.msg( 'templatedata-modal-button-delparam' ) )
.addClass( 'tdg-param-del' )
.attr( 'id', 'tdg-param-del' )
.data( 'paramid', paramName );
}
/**
* Checks the wikitext for template parameters and imports
* those that aren't yet in the templatedata list.
* Adapted from https://he.wikipedia.org/wiki/MediaWiki:Gadget-TemplateParamWizard.js
*
* @param {string} wikitext The source of the template text
*/
function importTemplateParams( wikitext ) {
var newParam, matches, $row, paramName, paramID,
paramExtractor = /{{3,}(.*?)[<|}]/mg,
paramCounter = 0,
existingParamNamesArray = [];
// fill up the existingParamNameArray with GUI params
// So we can test against it while importing:
// We should go by param name, not param ID, because
// if the param is new, its id is new_randomString, and so
// the actual representation is the value of the name field.
for ( paramID in curr.paramDomElements ) {
paramName = $.trim( curr.paramDomElements[paramID].name.val() );
// Validate
if (
paramName.length > 0 &&
!paramName.match( /[\|=]|}}/ ) &&
!curr.paramDomElements[paramID].tdgMarkedForDeletion &&
$.inArray( paramName, existingParamNamesArray ) === -1
) {
existingParamNamesArray.push( paramName );
}
}
while ( ( matches = paramExtractor.exec( wikitext ) ) !== null ) {
paramName = $.trim( matches[1] );
// Make sure the template itself is not giving us bad params
if (
paramName.length === 0 &&
paramName.match( /[\|=]|}}/ )
) {
continue;
}
// Make sure the param doesn't already exist in the GUI
if ( $.inArray( paramName, existingParamNamesArray ) > -1 ) {
// This is dupe, ignore it
continue;
} else {
// Add name to the existingParamNamesArray
existingParamNamesArray.push( paramName );
// Add to domParams
newParam = addParam();
newParam.name.val( paramName );
$row = translateParamToRowDom( curr.paramsJson, newParam );
domObjects.$modalTable.append( $row );
paramCounter++;
}
}
if ( paramCounter === 0 ) {
showErrorModal( mw.msg( 'templatedata-modal-errormsg-import-noparams' ) );
} else {
showErrorModal( mw.msg( 'templatedata-modal-notice-import-numparams', paramCounter ) );
}
}
/**
* Create a <table> DOM with initial headings for the parameters
* The table headings will go by the paramBase
*
* @returns {jQuery} Table element
*/
function createParamTableDOM() {
var $tbl, attrib,
$tr = $( '<tr>' );
for ( attrib in paramBase ) {
$tr.append(
$( '<th>' )
.text( paramBase[attrib].label )
.addClass( 'tdg-title-' + attrib )
);
}
$tbl = $( '<table>' )
.addClass( 'tdg-editTable' )
.append( $tr );
return $tbl;
}
/**
* Create a <table> HTMLElement with initial headings for the parameters
* The table headings will go by the paramBase
*
* @param {Object} paramsJson Object with current parameter values
* @param {Object} paramAttrObj Object with parameter properties
* @returns {jQuery} Table element
*/
function translateParamToRowDom( paramsJson, paramAttrObj ) {
var $tdDom,
$trDom,
paramAttr,
paramid = paramAttrObj.delbutton.data( 'paramid' );
$trDom = $( '<tr>' )
.attr( 'id', 'param-' + paramid )
.data( 'paramid', paramid );
// Go over the attributes for <td>s
for ( paramAttr in paramAttrObj ) {
// Check if value already exists for this in the original json
if (
paramsJson.params &&
paramsJson.params[paramid] &&
paramsJson.params[paramid][paramAttr]
) {
// make sure we set the value correctly based on the DOM element
if ( paramAttrObj[paramAttr].prop( 'type' ) === 'checkbox' ) {
paramAttrObj[paramAttr].prop( 'checked', paramsJson.params[paramid][paramAttr] );
} else {
paramAttrObj[paramAttr].val( paramsJson.params[paramid][paramAttr] );
}
}
$tdDom = $( '<td>' ).addClass( 'tdg-attr-' + paramAttr );
// Add label to 'required' checkbox
if ( paramAttr === 'required' ) {
$tdDom.append(
$( '<label>' )
.attr( 'for', paramAttr + '_paramid_' + paramid )
.text( paramBase.required.label )
.prepend( paramAttrObj[paramAttr] )
);
} else {
$tdDom.append( paramAttrObj[paramAttr] );
}
$trDom.append( $tdDom );
}
// Set up the name
if ( paramsJson && curr.paramsJson.params && curr.paramsJson.params[paramid] ) {
$trDom.find( '.tdg-element-attr-name' ).val( paramid );
}
return $trDom;
}
/**
* Add an empty parameter to the paramDomElements list
*
* @returns {jQuery} Table row element
*/
function addParam() {
var attrib,
$tmpDom,
// Create a unique identifier for paramid
paramid = 'new_' + $.now();
// Add to the DOM object
curr.paramDomElements[paramid] = {};
for ( attrib in paramBase ) {
// Set up the DOM element
if ( attrib === 'type' ) {
$tmpDom = createTypeSelect( paramTypes );
} else {
$tmpDom = paramBase[attrib].dom;
}
curr.paramDomElements[paramid][attrib] = $tmpDom.clone( true );
curr.paramDomElements[paramid][attrib].data( 'paramid', paramid );
curr.paramDomElements[paramid][attrib].attr( 'id', attrib + '_paramid_' + paramid );
curr.paramDomElements[paramid][attrib].addClass( 'tdg-element-attr-' + attrib );
}
// Set up the 'delete' button
curr.paramDomElements[paramid].delbutton
.text( mw.msg( 'templatedata-modal-button-delparam' ) )
.addClass( 'tdg-param-del' )
.attr( 'id', 'tdg-param-del' )
.data( 'paramid', paramid );
return curr.paramDomElements[paramid];
}
/**
* Validate the Modal inputs before continuing to the actual 'apply'
*
* @returns {boolean} Passed validation
*/
function isFormValid() {
var paramID,
paramName,
paramNameArray = [],
passed = true,
paramProblem = false;
// Reset
$( '.tdgerror' ).removeClass( 'tdgerror' );
domObjects.$errorModalBox.empty().hide();
// Go over the paramDomElements object, look for:
// * Empty name fields
// * Duplicate *name* values:
// * Illegal characters in name fields: pipe, equal, }}
for ( paramID in curr.paramDomElements ) {
paramProblem = false;
// Trim:
paramName = curr.paramDomElements[paramID].name.val();
curr.paramDomElements[paramID].name.val( paramName );
// Ignore if the param was flagged for deletion
if ( curr.paramDomElements[paramID].tdgMarkedForDeletion ) {
continue;
}
// Name field is empty
if ( paramName.length === 0 ) {
passed = false;
paramProblem = true;
}
// Check for illegal characters in param name
if ( paramName.match( /[\|=]|}}/ ) ) {
passed = false;
paramProblem = true;
}
// Check for dupes
if ( $.inArray( paramName, paramNameArray ) > -1 ) {
// This is dupe!
passed = false;
paramProblem = true;
} else {
paramNameArray.push( paramName );
}
if ( paramProblem ) {
domObjects.$modalTable.find( '#param-' + paramID ).addClass( 'tdgerror' );
}
}
return passed;
}
/**
* Reset the GUI completely, including the domElements and the json
*/
function globalReset() {
// Reset Modal GUI
domObjects.$modalBox.empty();
domObjects.$errorModalBox.empty().hide();
// Reset vars
curr = {
paramDomElements: {},
paramsJson: {}
};
}
/**
* Take the amended JSON object and stringify it, putting
* it back into the original wikitext.
* @param {Object} newJsonObject Edited json object
* @param {String} originalWikitext The original wikitext
* @returns {String} Thew new wikitext
*/
function amendWikitext( newJsonObject, originalWikitext ) {
var finalOutput = '',
wikitext = originalWikitext || domObjects.wikitext;
// Check if we started with existing <templatedata> tags
if ( wikitext.match(
/(<templatedata>)([\s\S]*?)(<\/templatedata>)/i)
) {
// replace the <templatedata> tags
finalOutput = wikitext.replace(
/(<templatedata>)([\s\S]*?)(<\/templatedata>)/i,
'<templatedata>\n' + JSON.stringify( newJsonObject, null, ' ' ) + '\n</templatedata>'
);
} else {
// add <templatedata> tags
finalOutput = wikitext + '\n<templatedata>\n';
finalOutput += JSON.stringify( newJsonObject, null, ' ' );
finalOutput += '\n</templatedata>';
}
return finalOutput;
}
/**
* Apply the changes made to the parameters to the json
*
* @param {Object} originalJsonObject [description]
* @param {Object<String,jQuery>} modalDomElements The structure of the
* dom elements in the editor, sorted by parameter id and jQuery editable
* object
* @param {String} originalWikitext The original wikitext
* @param {Boolean} DoNotCheckForm if set to true, the system will not validate the form
* used mostly for tests.
* @returns {Object} Amended json object
*/
function applyChangeToJSON( originalJsonObject, modalDomElements, doNotCheckForm ) {
var paramid,
paramName,
paramProp,
$domEl,
domElements,
newValue,
paramObj,
propExists,
tmpjson,
// Compare the original to the new changes
outputJson = originalJsonObject || curr.paramsJson,
paramDomElements = modalDomElements || curr.paramDomElements;
// Validate
if ( !doNotCheckForm ) {
if ( !isFormValid() ) {
showErrorModal( mw.msg( 'templatedata-modal-errormsg' ) );
return;
}
}
// Update the description
if ( $( '.tdg-template-description' ).length ) {
outputJson.description = $( '.tdg-template-description' ).val();
}
// First check if there's outpuJson.params
if ( !outputJson.params ) {
outputJson.params = {};
}
// Go over the parameters, check if param was marked as deleted
// in curr.paramsJson
for ( paramid in paramDomElements ) {
domElements = paramDomElements[paramid];
// Get the name of the param
paramName = domElements.name.val();
// New parameter added
if ( !outputJson.params[paramid] ) {
paramObj = outputJson.params[paramName] = {};
} else {
// Check if name changed
if ( paramName !== paramid ) {
// change the param name
outputJson.params[paramName] = {};
tmpjson = $.extend( true, {}, outputJson.params[paramid] );
$.extend( true, outputJson.params[paramName], tmpjson );
// delete the old param
delete outputJson.params[paramid];
}
}
// Parameter marked for deletion
if ( domElements.tdgMarkedForDeletion ) {
delete outputJson.params[paramName];
// Move to next iteration
continue;
}
paramObj = outputJson.params[paramName];
// Go over the properties that have DOM elements
for ( paramProp in domElements ) {
propExists = ( paramObj.hasOwnProperty( paramProp ) );
$domEl = domElements[paramProp];
// Check if value changed
switch ( paramProp ) {
// Skip:
case 'name':
case 'delbutton':
break;
case 'aliases':
newValue = cleanupAliasArray( $domEl.val() );
if ( propExists &&
newValue.sort().join( '|' ) !== paramObj.aliases.sort().join( '|' ) ) {
// Replace:
if ( newValue.length === 0 ) {
delete paramObj.aliases;
continue;
} else {
paramObj.aliases = newValue;
}
} else if ( !propExists ) {
if ( newValue.length > 0 ) {
paramObj.aliases = newValue;
}
}
break;
case 'description':
case 'default':
case 'label':
newValue = $domEl.val();
if ( paramObj[paramProp] !== newValue ) {
if ( !newValue || newValue.length === 0 ) {
delete paramObj[paramProp];
continue;
} else {
paramObj[paramProp] = newValue;
}
}
break;
case 'type':
newValue = $domEl.val();
if ( paramObj[paramProp] !== newValue ) {
if ( newValue === 'undefined' ) {
delete paramObj[paramProp];
continue;
} else {
paramObj[paramProp] = newValue;
}
}
break;
case 'required':
newValue = $domEl.prop( 'checked' );
if ( paramObj[paramProp] !== newValue ) {
paramObj[paramProp] = newValue;
}
break;
}
}
}
return outputJson;
}
/**
* Create i18n-compatible Modal Buttons
* also contains the 'apply' functionality
*
* @param {string} btnApply the text for the 'apply' button
* @param {string} btnCancel the text for the 'cancel' button
* @returns {Array} Button objects with their functionality, for the modal
*/
function i18nModalButtons( btnApply, btnCancel ) {
var modalButtons = {};
modalButtons[btnApply] = function() {
var newJson = applyChangeToJSON(),
finalOutput = amendWikitext( newJson );
// Close the modal
domObjects.$modalBox.dialog( 'close' );
// Trigger the closing event so the new output can be put
// back to the textbox
domObjects.$modalBox.trigger( 'TemplateDataGeneratorDone', [ finalOutput ] );
return finalOutput;
};
modalButtons[btnCancel] = function () {
domObjects.$modalBox.dialog( 'close' );
};
return modalButtons;
}
/** Public Methods **/
return {
/**
* Injects required DOM elements to the edit screen
*/
init: function () {
paramBase = {
name: {
label: mw.msg( 'templatedata-modal-table-param-name' ),
dom: $( '<input>' )
},
aliases: {
label: mw.msg( 'templatedata-modal-table-param-aliases' ),
dom: $( '<input>' )
},
label: {
label: mw.msg( 'templatedata-modal-table-param-label' ),
dom: $( '<input>' )
},
description: {
label: mw.msg( 'templatedata-modal-table-param-desc' ),
dom: $( '<textarea>' )
},
type: {
label: mw.msg( 'templatedata-modal-table-param-type' ),
dom: $( '<select>' )
},
'default': {
label: mw.msg( 'templatedata-modal-table-param-default' ),
dom: $( '<textarea>' )
},
'required': {
label: mw.msg( 'templatedata-modal-table-param-required' ),
dom: $( '<input type="checkbox" />' )
},
delbutton: {
label: mw.msg( 'templatedata-modal-table-param-actions' ),
dom: $( '<button>' )
.button()
.addClass( 'tdg-param-button-del buttonRed' )
.click( function () {
var paramid = $( this ).data( 'paramid' );
// Flag as DELETED in curr.paramDomElements[paramid] (property tdgDELETED)
if ( curr.paramDomElements[paramid] ) {
curr.paramDomElements[paramid].tdgMarkedForDeletion = true;
}
// Delete the DOM row from table:
// (Don't delete property from paramDomElements,
// so when we go over the DOM elements on 'apply' this
// parameter is recognized as marked for deletion)
$( '#param-' + paramid ).remove();
} )
}
};
paramTypes = {
'undefined': mw.msg( 'templatedata-modal-table-param-type-undefined' ),
'number': mw.msg( 'templatedata-modal-table-param-type-number' ),
'string': mw.msg( 'templatedata-modal-table-param-type-string' ),
'string/wiki-user-name': mw.msg( 'templatedata-modal-table-param-type-user' ),
'string/wiki-page-name': mw.msg( 'templatedata-modal-table-param-type-page' )
};
domObjects = {
$editButton: $( '<button>' )
.button()
.addClass( 'tdg-editscreen-main-button' )
.text( mw.msg( 'templatedata-editbutton' ) ),
$errorBox: $( '<div>' )
.addClass( 'tdg-editscreen-error-msg' )
.hide(),
$errorModalBox: $( '<div>' )
.addClass( 'tdg-errorbox' )
.hide(),
$modalBox: $( '<div>' )
.addClass( 'tdg-editscreen-modal-form' )
.attr( 'id', 'modal-box' )
.attr( 'title', mw.msg( 'templatedata-modal-title' ) )
.hide(),
$modalTable: {},
wikitext: ''
};
curr = {
paramDomElements: {},
paramsJson: {}
};
// Return the objects to be added to the edit page
return domObjects;
},
/**
* Create the modal screen and populate it with existing
* data, if available
*
* @param {jQuery} $wikitextBox Article edit textarea
* @returns {jQuery} Modal div element
*/
createModal: function ( wikitext ) {
var $row,
paramObj,
$descBox;
// Reset:
globalReset();
domObjects.wikitext = wikitext;
$descBox = $( '<textarea>' ).addClass( 'tdg-template-description' );
domObjects.$modalTable = createParamTableDOM();
// Parse JSON
curr.paramsJson = parseTemplateData( wikitext );
if ( !$.isEmptyObject( curr.paramsJson ) ) {
if ( curr.paramsJson.description ) {
$descBox.text( curr.paramsJson.description );
}
// Build the parameter row DOMs
for ( paramObj in curr.paramDomElements ) {
// Make the row
$row = translateParamToRowDom( curr.paramsJson, curr.paramDomElements[paramObj] );
domObjects.$modalTable.append( $row );
}
}
// Build the Modal window
domObjects.$modalBox
.append( $( '<h3>' )
.addClass( 'tdg-title' )
.text( mw.msg( 'templatedata-modal-title-templatedesc' ) )
)
.append( $descBox )
.append( domObjects.$errorModalBox )
.append( $( '<h3>' )
.addClass( 'tdg-title' )
.text( mw.msg( 'templatedata-modal-title-templateparams' ) )
)
.append(
$( '<button>' )
.button()
.text( mw.msg( 'templatedata-modal-button-importParams' ) )
.addClass( 'tdg-addparam' )
.click( function () {
// TODO: Check that we're not in the /doc subpage
importTemplateParams( wikitext );
} ) )
.append( domObjects.$modalTable )
.append(
$( '<button>' )
.button()
.text( mw.msg( 'templatedata-modal-button-addparam' ) )
.addClass( 'tdg-addparam' )
.click( function () {
var newParam = addParam(),
$row = translateParamToRowDom( curr.paramsJson, newParam );
domObjects.$modalTable.append( $row );
} )
);
domObjects.$modalBox.dialog( {
autoOpen: false,
height: $( window ).height() * 0.8,
width: $( window ).width() * 0.8,
modal: true,
buttons: i18nModalButtons(
mw.msg( 'templatedata-modal-button-apply' ),
mw.msg( 'templatedata-modal-button-cancel' )
),
close: function () {
domObjects.$modalBox.empty();
}
} );
// Return the modal object
return domObjects.$modalBox;
},
/** Testing functions **/
/**
* @private
* @inheritDoc #parseTemplateData
*/
parseTemplateData: function( wikitext ) {
return parseTemplateData( wikitext );
},
/**
* @private
* @inheritDoc #applyChangesToJSON
*/
applyChangesToJSON: function( originalJsonObject, modalDomElements, doNotCheckForm ) {
return applyChangeToJSON( originalJsonObject, modalDomElements, doNotCheckForm );
},
/**
* @private
* @inheritDoc #amendWikitext
*/
amendWikitext: function( newJsonObject, originalWikitext ) {
return amendWikitext( newJsonObject, originalWikitext );
},
/**
* @private
* @inheritDoc #translateParamToRowDom
*/
translateParamToRowDom: function( paramsJson, paramAttrObj ) {
return translateParamToRowDom( paramsJson, paramAttrObj );
}
};
} )();
}( jQuery, mediaWiki ) );

View file

@ -0,0 +1,87 @@
.tdg-editscreen-main-button {
padding: 5px;
font-size: 120%;
}
.tdg-editTable {
width: 100%;
}
.tdg-editTable th, .tdg-editTable td{
padding:0;
}
.tdg-editTable > tbody > tr {
vertical-align: top;
}
.tdg-editTable th {
text-align: left;
}
.tdg-editTable > tbody > tr > td:hover,
.tdg-editTable > tbody > tr:hover > td {
background-color: #BECDD4;
}
.tdg-editTable input,
.tdg-editTable select,
.tdg-editTable textarea,
.tdg-template-description{
background: transparent;
border: 1px solid #999999;
padding: 5px;
box-sizing: border-box;
-moz-box-sizing: border-box;
width: 100%;
}
.tdg-editTable input:not([type="checkbox"]),
.tdg-editTable select,
.tdg-editTable textarea{
height: 40px;
}
.tdg-editTable .tdg-title-delbutton, .tdg-title-required {
width: 80px;
}
.tdg-editTable .tdg-attr-required {
text-align: center;
}
.tdg-editTable .tdg-attr-required label {
display: block;
}
.tdg-attr-type select {
padding-top: 15px;
}
.tdg-attr-type {
width: 130px;
text-align: center;
}
.tdg-editTable .tdgerror {
background-color: #F2CBCB;
}
/** Start small, grow on focus: */
.tdg-title-name,
.tdg-title-aliases,
.tdg-title-label {
width: 100px;
}
.tdg-element-attr-name,
.tdg-element-attr-aliases,
.tdg-element-attr-label {
width: 100px;
}
.tdg-element-attr-name:focus,
.tdg-element-attr-aliases:focus,
.tdg-element-attr-label:focus {
width: 200px;
}
.tdg-element-attr-description:focus,
.tdg-element-attr-default:focus {
height: 70px;
}

View file

@ -0,0 +1,43 @@
( function ( $, mw ) {
/**
* TemplateData Generator button fixture
* The button will appear on Template namespaces only, above the edit textbox
*
* @author Moriel Schottlender
*/
'use strict';
$( document ).ready(function () {
var tmplDataGen, editboxObjects,
$textbox = $( '#wpTextbox1' );
// Check if there's an editor textarea and if we're in the proper namespace
if ( $textbox.length > 0 && mw.config.get( 'wgCanonicalNamespace' ) === 'Template' ) {
tmplDataGen = mw.libs.templateDataGenerator;
editboxObjects = tmplDataGen.init();
// Add the button and modal element to the document
$( '#mw-content-text' )
.prepend(
editboxObjects.$modalBox,
editboxObjects.$errorBox,
editboxObjects.$editButton
);
$( '.tdg-editscreen-main-button' ).click( function () {
var $modalBox = tmplDataGen.createModal( $textbox.val() );
// open the dialog
$modalBox.dialog( 'open' );
// respond to modal close event
$modalBox.on( 'TemplateDataGeneratorDone', function( e, output ) {
$textbox.val( output );
} );
} );
}
} );
}( jQuery, mediaWiki ) );

View file

@ -0,0 +1,269 @@
/**
* TemplateData Generator GUI Unit Tests
*/
( function ( $, mw ) {
'use strict';
QUnit.module( 'ext.templateData', QUnit.newMwEnvironment() );
var wikitext = 'Some initial test sentence.\n' +
'<templatedata>\n' +
'{\n' +
' "description": "This is a description of the template.",\n' +
' "params": {\n' +
' "user": {\n' +
' "label": "Username",\n' +
' "type": "string/wiki-user-name",\n' +
' "default": "some default value here.",\n' +
' "required": true,\n' +
' "description": "User name of person who forgot to sign their comment.",\n' +
' "aliases": ["1"]\n' +
' },\n' +
' "date": {\n' +
' "label": "Date",\n' +
' "aliases": ["2", "3"]\n' +
' },\n' +
' "year": {\n' +
' "label": "Year",\n' +
' "type": "number"\n' +
' },\n' +
' "comment": {\n' +
' "required": false\n' +
' }\n' +
' }\n' +
'}\n' +
'</templatedata>\n' +
'Some following sentence.';
QUnit.test( 'TemplateData modal display', 11, function ( assert ) {
var $modalBox, tmplDataGenTest1;
tmplDataGenTest1 = mw.libs.templateDataGenerator;
tmplDataGenTest1.init();
$modalBox = tmplDataGenTest1.createModal( wikitext );
// Tests
assert.equal(
$modalBox.find( '.tdg-template-description' ).val(),
'This is a description of the template.',
'Template description'
);
assert.equal(
$modalBox.find( '.tdg-element-attr-name' ).length,
4,
'Number of parameters in edit modal table.'
);
// Check for proper parsing
assert.equal(
$modalBox.find( '#param-user .tdg-element-attr-name' ).val(),
'user',
'Parameter details: name.'
);
assert.equal(
$modalBox.find( '#param-date .tdg-element-attr-aliases' ).val(),
'2,3',
'Parameter details: aliases (multiple).'
);
assert.equal(
$modalBox.find( '#param-user .tdg-element-attr-aliases' ).val(),
'1',
'Parameter details: aliases (single).'
);
assert.equal(
$modalBox.find( '#param-user .tdg-element-attr-label' ).val(),
'Username',
'Parameter details: label.'
);
assert.equal(
$modalBox.find( '#param-user .tdg-element-attr-description' ).val(),
'User name of person who forgot to sign their comment.',
'Parameter details: description.'
);
assert.equal(
$modalBox.find( '#param-user .tdg-element-attr-type' ).val(),
'string/wiki-user-name',
'Parameter details: type.'
);
assert.equal(
$modalBox.find( '#param-user .tdg-element-attr-default' ).val(),
'some default value here.',
'Parameter details: default.'
);
assert.equal(
$modalBox.find( '#param-user .tdg-element-attr-required' ).prop( 'checked' ),
true,
'Parameter details: required.'
);
assert.equal(
$modalBox.find( '#param-year .tdg-element-attr-required' ).prop( 'checked' ),
false,
'Parameter details: non required.'
);
} );
QUnit.test( 'TemplateData JSON manipulation', 4, function ( assert ) {
var $modalDomElements, parsedJson, expectedTextResult,
exampleJson, changedParsedJson, reparsedJson,
tmplDataGenTest2 = mw.libs.templateDataGenerator,
origText = 'Some initial test sentence.\n' +
'<templatedata>\n' +
'{\n' +
' "description": "This is a description of the template.",\n' +
' "params": {\n' +
' "user": {\n' +
' "label": "Username",\n' +
' "type": "string/wiki-user-name",\n' +
' "default": "some default value here.",\n' +
' "required": true,\n' +
' "description": "User name of person who forgot to sign their comment.",\n' +
' "aliases": ["1"]\n' +
' },\n' +
' "date": {\n' +
' "label": "Date",\n' +
' "aliases": ["2", "3"]\n' +
' },\n' +
' "year": {\n' +
' "label": "Year",\n' +
' "type": "number"\n' +
' },\n' +
' "comment": {\n' +
' "required": false,\n' +
' "somethingelse": "this should stay"\n' +
' }\n' +
' },\n' +
' "testing": {\n' +
' "something": {\n' +
' "completely": "random"\n' +
' }\n' +
' }\n' +
'}\n' +
'</templatedata>\n' +
'Some following sentence.';
parsedJson = tmplDataGenTest2.parseTemplateData( origText );
assert.equal(
parsedJson.testing.something.completely,
'random',
'Parse original JSON and preserve all data.'
);
// Copy the parsed JSON object so we can manually
// manipulate it
changedParsedJson = $.extend( true, {}, parsedJson );
// Partial dom elements on purpose, to make sure
// that the rest of the json object, even fields that are
// not represented in the dom elements, are preserved
$modalDomElements = {
'user': {
'name': $( '<input>' ).val( 'user' ),
'label': $( '<input>' ).val( 'changed to another label' ),
}
};
// Manually change the object we copied earlier to test against
changedParsedJson.params.user.label = 'changed to another label';
assert.deepEqual(
mw.libs.templateDataGenerator.applyChangesToJSON( parsedJson, $modalDomElements, true ),
changedParsedJson,
'Preserve parameters on edit.'
);
// Name change
// Notice, parsedJson also had its user.label change, so we have
// to do the same to our new test and change both label and name.
$modalDomElements = {
'user': {
'name': $( '<input>' ).val( 'anotherName' ),
'label': $( '<input>' ).val( 'changed to another label' ),
}
};
// Copy the parsed JSON object so we can manually
// manipulate it
changedParsedJson = $.extend( true, {}, parsedJson );
changedParsedJson.params.anotherName = {};
$.extend( true, changedParsedJson.params.anotherName, parsedJson.params.user );
delete changedParsedJson.params.user;
// Re-parse the json so we can manipulate it in exampleJson
reparsedJson = mw.libs.templateDataGenerator.parseTemplateData( origText );
// Get the system's response to changing the name
exampleJson = mw.libs.templateDataGenerator.applyChangesToJSON( reparsedJson, $modalDomElements, true );
assert.deepEqual(
exampleJson,
changedParsedJson,
'Change parameter name.'
);
// Back to wikitext
// Since 'parsedJson' was changed in previous tests, we'll use it
expectedTextResult = 'Some initial test sentence.\n' +
'<templatedata>\n' +
'{\n' +
' "description": "This is a description of the template.",\n' +
' "params": {\n' +
' "date": {\n' +
' "label": "Date",\n' +
' "aliases": [\n' +
' "2",\n' +
' "3"\n' +
' ]\n' +
' },\n' +
' "year": {\n' +
' "label": "Year",\n' +
' "type": "number"\n' +
' },\n' +
' "comment": {\n' +
' "required": false,\n' +
' "somethingelse": "this should stay"\n' +
' },\n' +
' "anotherName": {\n' +
' "label": "changed to another label",\n' +
' "type": "string/wiki-user-name",\n' +
' "default": "some default value here.",\n' +
' "required": true,\n' +
' "description": "User name of person who forgot to sign their comment.",\n' +
' "aliases": [\n' +
' "1"\n' +
' ]\n' +
' }\n' +
' },\n' +
' "testing": {\n' +
' "something": {\n' +
' "completely": "random"\n' +
' }\n' +
' }\n' +
'}\n' +
'</templatedata>\n' +
'Some following sentence.';
// Using 'exampleJson' with the previous name change and label change
assert.equal(
mw.libs.templateDataGenerator.amendWikitext( exampleJson, origText ),
expectedTextResult,
'Returning edited json into original wikitext.'
);
} );
}( jQuery, mediaWiki ) );