Viewing File: /home/omtekel/www/wp-content/upgrade/backup/TemplateData.tar
modules/ext.templateDataGenerator.data/SourceHandler.js 0000666 00000020050 15133472347 0017305 0 ustar 00 var Model = require( './Model.js' );
/**
* TemplateData Source Handler
*
* Loads and validates the templatedata and template parameters
* whether in the page itself or from its parent.
*
* @class
* @extends OO.EventEmitter
*
* @constructor
* @param {Object} [config]
* @cfg {string} [fullPageName] The full name of the current page
* @cfg {string} [parentPage] The name of the parent page
* @cfg {string} [isPageSubLevel] The page is sub-level of another template
*/
function SourceHandler( config ) {
config = config || {};
// Mixin constructors
OO.EventEmitter.call( this );
this.apiCache = {};
this.templateSourceCodePromise = null;
this.templateSourceCodeParams = [];
// Config
this.setParentPage( config.parentPage );
this.setPageSubLevel( config.isPageSubLevel );
this.setFullPageName( config.fullPageName );
}
/* Inheritance */
OO.mixinClass( SourceHandler, OO.EventEmitter );
/**
* Get information from the MediaWiki API
*
* @param {string} page Page name
* @param {boolean} [getTemplateData] Fetch the templatedata in the page.
* @return {jQuery.Promise} API promise
*/
SourceHandler.prototype.getApi = function ( page, getTemplateData ) {
var type = getTemplateData ? 'templatedata' : 'query',
api = new mw.Api(),
baseConfig = {
action: type,
titles: page,
redirects: getTemplateData ? 1 : 0
};
var config;
if ( type === 'query' ) {
config = $.extend( baseConfig, {
prop: 'revisions',
rvprop: 'content',
indexpageids: '1'
} );
}
// Cache
if ( !this.apiCache[ page ] || !this.apiCache[ page ][ type ] ) {
this.apiCache[ page ] = this.apiCache[ page ] || {};
this.apiCache[ page ][ type ] = api.get( config );
}
return this.apiCache[ page ][ type ];
};
/**
* Go over the current wikitext and build a new model.
*
* @param {string} [wikitext] Source of the template.
* @return {jQuery.Promise} Promise resolving into a new Model
* or is rejected if the model was impossible to create.
*/
SourceHandler.prototype.buildModel = function ( wikitext ) {
var tdObject = null,
templateDataString = this.findModelInString( wikitext );
if ( templateDataString !== null ) {
try {
tdObject = JSON.parse( templateDataString );
} catch ( err ) {
// The json object is invalid. There's no need to continue.
return $.Deferred().reject();
}
}
// Get parameters from source code
// Mostly used for the import option
return this.getParametersFromTemplateSource( wikitext )
// This is always successful by definition
.then( function ( templateSourceCodeParams ) {
return Model.static.newFromObject(
tdObject,
templateSourceCodeParams
);
} );
};
/**
* Retrieve parameters from the template code from source in this order:
*
* 1. Check if there's a template in the given 'wikitext' parameter. If not,
* 2. Check if there's a template in the current page. If not,
* 3. Check if the page is a subpage and go up a level to check for template code. If none found,
* 4. Repeat until we are in the root of the template
* 5. Save the name of the page where the template is taken from
*
* Cache the templateCodePromise so we don't have to do this all over again on each
* template code request.
*
* @param {string} [wikitext] Optional. Source of the template.
* @return {jQuery.Promise} Promise resolving into template parameter array
*/
SourceHandler.prototype.getParametersFromTemplateSource = function ( wikitext ) {
var params = [],
sourceHandler = this;
if ( !this.templateSourceCodePromise ) {
// Check given page text first
if ( wikitext ) {
params = this.extractParametersFromTemplateCode( wikitext );
}
if ( params.length > 0 ) {
// There are parameters found; Resolve.
this.templateSourceCodePromise = $.Deferred().resolve( params );
} else if ( this.isPageSubLevel() && this.getParentPage() ) {
// Get the content of the parent
this.templateSourceCodePromise = this.getApi( this.getParentPage() ).then(
function ( resp ) {
var pageContent = '';
// Verify that we have a sane response from the API.
// This is particularly important for unit tests, since the
// requested page from the API is the Qunit module and has no content
if (
resp.query.pages[ resp.query.pageids[ 0 ] ].revisions &&
resp.query.pages[ resp.query.pageids[ 0 ] ].revisions[ 0 ]
) {
pageContent = resp.query.pages[ resp.query.pageids[ 0 ] ].revisions[ 0 ][ '*' ];
}
return sourceHandler.extractParametersFromTemplateCode( pageContent );
},
function () {
// Resolve an empty parameters array
return $.Deferred().resolve( [] );
}
);
} else {
// No template found. Resolve to empty array of parameters
this.templateSourceCodePromise = $.Deferred().resolve( [] );
}
}
return this.templateSourceCodePromise;
};
/**
* Retrieve template parameters from the template code.
*
* Adapted from https://he.wikipedia.org/wiki/MediaWiki:Gadget-TemplateParamWizard.js
*
* @param {string} templateCode Source of the template.
* @return {string[]} An array of parameters that appear in the template code
*/
SourceHandler.prototype.extractParametersFromTemplateCode = function ( templateCode ) {
var paramNames = [],
normalizedParamNames = [],
// This regex matches the one in TemplateDataBlob.php
paramExtractor = /{{{+([^\n#={|}]*?)([<|]|}}})/mg;
// Ignore non-wikitext content in comments and wikitext-escaping tags
templateCode = templateCode.replace( /<!--[\s\S]*?-->/g, '' )
.replace( /<nowiki\s*>[\s\S]*?<\/nowiki\s*>/g, '' )
.replace( /<pre\s*>[\s\S]*?<\/pre\s*>/g, '' );
var matches;
while ( ( matches = paramExtractor.exec( templateCode ) ) !== null ) {
// This normalization process is repeated in PHP in TemplateDataBlob.php
var normalizedParamName = matches[ 1 ].replace( /[-_ ]+/, ' ' ).trim().toLowerCase();
if ( !normalizedParamName || normalizedParamNames.indexOf( normalizedParamName ) !== -1 ) {
continue;
}
if ( paramNames.indexOf( matches[ 1 ] ) === -1 ) {
normalizedParamNames.push( normalizedParamName );
paramNames.push( matches[ 1 ].trim() );
}
}
return paramNames;
};
/**
* Look for a templatedata json string and return it, if it exists.
*
* @param {string} templateDataString Wikitext templatedata string
* @return {string|null} The isolated json string. Empty if no
* templatedata string was found.
*/
SourceHandler.prototype.findModelInString = function ( templateDataString ) {
var parts = templateDataString.match(
/<templatedata>([\s\S]*?)<\/templatedata>/i
);
if ( parts && parts[ 1 ] && parts[ 1 ].trim().length > 0 ) {
return parts[ 1 ].trim();
} else {
return null;
}
};
/**
* Set the page as a sub page of the main template
*
* @param {boolean} isSubLevel Page is sublevel
*/
SourceHandler.prototype.setPageSubLevel = function ( isSubLevel ) {
this.subLevel = !!isSubLevel;
};
/**
* Set the page as a sub page of the main template
*
* @return {boolean} Page is sublevel
*/
SourceHandler.prototype.isPageSubLevel = function () {
return this.subLevel;
};
/**
* Get full page name
*
* @param {string} pageName Page name
*/
SourceHandler.prototype.setFullPageName = function ( pageName ) {
this.fullPageName = pageName || '';
};
/**
* Get page full name
*
* @return {string} Page full name
*/
SourceHandler.prototype.getFullPageName = function () {
return this.fullPageName;
};
/**
* Set parent page
*
* @param {string} parent Parent page
*/
SourceHandler.prototype.setParentPage = function ( parent ) {
this.parentPage = parent || '';
};
/**
* Get parent page
*
* @return {string} Parent page
*/
SourceHandler.prototype.getParentPage = function () {
return this.parentPage;
};
/**
* Set template source code parameters
*
* @param {string[]} params Parameters from the template source code
*/
SourceHandler.prototype.setTemplateSourceCodeParams = function ( params ) {
this.templateSourceCodeParams = params;
};
/**
* Set template source code parameters
*
* @return {string[]} Parameters from the template source code
*/
SourceHandler.prototype.getTemplateSourceCodeParams = function () {
return this.templateSourceCodeParams;
};
module.exports = SourceHandler;
modules/ext.templateDataGenerator.data/init.js 0000666 00000000233 15133472347 0015513 0 ustar 00 var Model = require( './Model.js' ),
SourceHandler = require( './SourceHandler.js' );
module.exports = {
Model: Model,
SourceHandler: SourceHandler
};
modules/ext.templateDataGenerator.data/Model.js 0000666 00000073544 15133472347 0015627 0 ustar 00 /**
* TemplateData Model
*
* @class
* @mixes OO.EventEmitter
*
* @constructor
*/
function Model() {
// Mixin constructors
OO.EventEmitter.call( this );
// Properties
this.description = {};
this.maps = undefined;
this.mapsChanged = false;
this.originalMaps = undefined;
this.format = null;
this.params = {};
this.paramIdentifierCounter = 2;
this.sourceCodeParameters = [];
this.paramOrder = [];
this.paramOrderChanged = false;
this.originalTemplateDataObject = null;
}
/* Inheritance */
OO.mixinClass( Model, OO.EventEmitter );
/* Events */
/**
* @event add-param
* @param {string} key Parameter key
* @param {Object} data Parameter data
*/
/**
* @event change-description
* @param {string} description New template description
* @param {Object} [language] Description language, if supplied
*/
/**
* @event change-paramOrder
* @param {string[]} orderArray Parameter key array in order
*/
/**
* @event change-property
* @param {string} paramKey Parameter key
* @param {string} prop Property name
* @param {Mixed} value
* @param {string} language
*/
/**
* @event change
*/
/* Static Methods */
/**
* Compare two objects or strings
*
* @param {Object|string} obj1 Base object
* @param {Object|string} obj2 Compared object
* @param {boolean} [allowSubset] Allow the second object to be a
* partial object (or a subset) of the first.
* @return {boolean} Objects have equal values
*/
Model.static.compare = function ( obj1, obj2, allowSubset ) {
if ( allowSubset && obj2 === undefined ) {
return true;
}
// Make sure the objects are of the same type
if ( typeof obj1 !== typeof obj2 ) {
return false;
}
// Comparing objects or arrays
if ( typeof obj1 === 'object' ) {
return OO.compare( obj2, obj1, allowSubset );
}
// Everything else (primitive types, functions, etc)
return obj1 === obj2;
};
/**
* Translate obsolete parameter types into the new types
*
* @param {string} paramType Given type
* @return {string} Normalized non-obsolete type
*/
Model.static.translateObsoleteParamTypes = function ( paramType ) {
return paramType.replace( /^string\//, '' );
};
/**
* Retrieve information about all legal properties for a parameter.
*
* @param {boolean} getFullData Retrieve full information about each
* parameter. If false, the method will return an array of property
* names only.
* @return {Object|string[]} Legal property names with or without their
* definition data
*/
Model.static.getAllProperties = function ( getFullData ) {
var properties = {
name: {
type: 'string',
// Validation regex
restrict: /[|=]|}}/
},
aliases: {
type: 'array'
},
label: {
type: 'string',
allowLanguages: true
},
description: {
type: 'string',
allowLanguages: true
},
example: {
type: 'string',
allowLanguages: true
},
type: {
type: 'select',
children: [
'unknown',
'boolean',
'content',
'wiki-file-name',
'line',
'number',
'date',
'wiki-page-name',
'string',
'wiki-template-name',
'unbalanced-wikitext',
'url',
'wiki-user-name'
],
default: 'unknown'
},
suggestedvalues: {
type: 'array'
},
default: {
type: 'string',
multiline: true,
allowLanguages: true
},
autovalue: {
type: 'string'
},
deprecated: {
type: 'boolean',
// This should only be defined for boolean properties.
// Define the property that represents the text value.
textValue: 'deprecatedValue'
},
deprecatedValue: {
type: 'string'
},
required: {
type: 'boolean'
},
suggested: {
type: 'boolean'
}
};
if ( !getFullData ) {
return Object.keys( properties );
} else {
return properties;
}
};
/**
* Retrieve the list of property names that allow for multiple languages.
*
* @return {string[]} Property names
*/
Model.static.getPropertiesWithLanguage = function () {
var result = [],
propDefinitions = this.getAllProperties( true );
for ( var prop in propDefinitions ) {
if ( propDefinitions[ prop ].allowLanguages ) {
result.push( prop );
}
}
return result;
};
/**
* Split a string into an array and clean/trim the values
*
* @param {string} str String to split
* @param {string} [delim] Delimeter
* @return {string[]} Clean array
*/
Model.static.splitAndTrimArray = function ( str, delim ) {
delim = delim || mw.msg( 'comma-separator' );
var arr = [];
str.split( delim ).forEach( function ( part ) {
var trimmed = part.trim();
if ( trimmed ) {
arr.push( trimmed );
}
} );
return arr;
};
/**
* This is an adjustment of OO.simpleArrayUnion that ignores
* empty values when inserting into the unified array.
*
* @param {...Array} arrays Arrays to union
* @return {Array} Union of the arrays
*/
Model.static.arrayUnionWithoutEmpty = function () {
var result = OO.simpleArrayUnion.apply( this, arguments );
// Trim and filter empty strings
return result.filter( function ( i ) {
return i.trim();
} );
};
/**
* Create a new mwTemplateData.Model from templatedata object.
*
* @param {Object|null} tdObject TemplateData parsed object, or null if we are creating a new object.
* @param {string[]} paramsInSource Parameter names found in template source
* @return {Model} New model
*/
Model.static.newFromObject = function ( tdObject, paramsInSource ) {
var model = new Model();
model.setSourceCodeParameters( paramsInSource || [] );
// Store the original templatedata object for comparison later
model.setOriginalTemplateDataObject( tdObject );
tdObject = tdObject || { params: {} };
// Initialize the model
model.params = {};
// Add params
if ( tdObject.params ) {
for ( var param in tdObject.params ) {
model.addParam( param, tdObject.params[ param ] );
}
}
// maps
if ( tdObject.maps ) {
model.setMapInfo( tdObject.maps );
}
model.setTemplateDescription( tdObject.description );
// Override the param order if it exists in the templatedata string
if ( tdObject.paramOrder && tdObject.paramOrder.length > 0 ) {
model.setTemplateParamOrder( tdObject.paramOrder );
}
if ( tdObject.format !== undefined ) {
model.setTemplateFormat( tdObject.format );
}
return model;
};
/* Methods */
/**
* Go over the importable parameters and check if they are
* included in the parameter model. Return the parameter names
* that are not included yet.
*
* @return {string[]} Parameters that are not yet included in
* the model
*/
Model.prototype.getMissingParams = function () {
var allParamNames = this.getAllParamNames(),
sourceCodeParameters = this.sourceCodeParameters;
return sourceCodeParameters.filter( function ( sourceCodeParameter ) {
return allParamNames.indexOf( sourceCodeParameter ) === -1;
} );
};
/**
* Add imported parameters into the model
*
* @return {Object} Parameters added. -1 for failure.
*/
Model.prototype.importSourceCodeParameters = function () {
var model = this,
allParamNames = this.getAllParamNames(),
existingArray = [],
importedArray = [],
skippedArray = [];
// Check existing params
allParamNames.forEach( function ( paramKey ) {
if ( model.sourceCodeParameters.indexOf( paramKey ) !== -1 ) {
existingArray.push( paramKey );
}
} );
// Add sourceCodeParameters to the model
this.sourceCodeParameters.forEach( function ( sourceCodeParameter ) {
if ( existingArray.indexOf( sourceCodeParameter ) === -1 ) {
model.addParam( sourceCodeParameter );
importedArray.push( sourceCodeParameter );
} else {
skippedArray.push( sourceCodeParameter );
}
} );
return {
imported: importedArray,
skipped: skippedArray,
existing: existingArray
};
};
/**
* Retrieve all existing language codes in the current templatedata model
*
* @return {string[]} Language codes in use
*/
Model.prototype.getExistingLanguageCodes = function () {
var result = [];
// Take languages from the template description
if ( $.isPlainObject( this.description ) ) {
result = Object.keys( this.description );
}
var languageProps = this.constructor.static.getPropertiesWithLanguage();
// Go over the parameters
for ( var param in this.params ) {
// Go over the properties
for ( var prop in this.params[ param ] ) {
if ( languageProps.indexOf( prop ) !== -1 ) {
result = this.constructor.static.arrayUnionWithoutEmpty( result, Object.keys( this.params[ param ][ prop ] ) );
}
}
}
return result;
};
/**
* Add parameter to the model
*
* @param {string} key Parameter key
* @param {Object} [paramData] Parameter data
* @fires add-param
* @fires change
*/
Model.prototype.addParam = function ( key, paramData ) {
var existingNames = this.getAllParamNames(),
data = $.extend( true, {}, paramData );
var name = key;
// Check that the parameter is not already in the model
if ( this.params[ key ] || existingNames.indexOf( key ) !== -1 ) {
// Change parameter key
key = this.getNewValidParameterKey( key );
}
// Initialize
this.params[ key ] = {};
// Store the key
this.params[ key ].name = name;
// Mark the parameter if it is in the template source
if ( this.sourceCodeParameters.indexOf( key ) !== -1 ) {
this.params[ key ].inSource = true;
}
// Translate types
if ( data.type !== undefined ) {
data.type = this.constructor.static.translateObsoleteParamTypes( data.type );
this.params[ key ].type = data.type;
}
// Get the deprecated value
if ( typeof data.deprecated === 'string' ) {
this.params[ key ].deprecatedValue = data.deprecated;
}
// Go over the rest of the data
if ( data ) {
var language = this.getDefaultLanguage();
var propertiesWithLanguage = this.constructor.static.getPropertiesWithLanguage();
var allProps = this.constructor.static.getAllProperties( true );
for ( var prop in data ) {
var propToSet = prop;
if (
// This is to make sure that forwards compatibility is achieved
// and the code doesn't die on properties that aren't valid
allProps[ prop ] &&
// Check if property should have its text represented in another internal property
// (for example, deprecated and deprecatedValue)
allProps[ prop ].textValue
) {
// Set the textValue property
propToSet = allProps[ prop ].textValue;
// Set the boolean value in the current property
this.setParamProperty( key, prop, !!data[ prop ], language );
if ( typeof data[ prop ] === 'boolean' ) {
// Only set the value of the dependent if the value is a string or
// language. Otherwise, if the value is boolean, keep the dependent
// empty.
continue;
}
}
if (
propertiesWithLanguage.indexOf( propToSet ) !== -1 &&
$.isPlainObject( data[ prop ] )
) {
// Add all language properties
for ( var lang in data[ prop ] ) {
this.setParamProperty( key, propToSet, data[ prop ], lang );
}
} else {
this.setParamProperty( key, propToSet, data[ prop ], language );
}
}
}
// Add to paramOrder
this.addKeyTemplateParamOrder( key );
// Trigger the add parameter event
this.emit( 'add-param', key, this.params[ key ] );
this.emit( 'change' );
};
/**
* Retrieve an array of all used parameter names. Note that parameter
* names can be different than their stored keys.
*
* @return {string[]} Used parameter names
*/
Model.prototype.getAllParamNames = function () {
var result = [];
for ( var key in this.params ) {
var param = this.params[ key ];
result.push( param.name );
if ( param.aliases ) {
result = result.concat( param.aliases );
}
}
return result;
};
/**
* Set the template description
*
* @param {string|Object} desc New template description
* @param {string} [language] Description language, if supplied. If not given,
* will default to the wiki language.
* @fires change-description
* @fires change
*/
Model.prototype.setTemplateDescription = function ( desc, language ) {
language = language || this.getDefaultLanguage();
if ( !this.constructor.static.compare( this.description[ language ], desc ) ) {
if ( typeof desc === 'object' ) {
$.extend( this.description, desc );
this.emit( 'change-description', desc[ language ], language );
} else {
this.description[ language ] = desc;
this.emit( 'change-description', desc, language );
}
this.emit( 'change' );
}
};
/**
* Get the template description.
*
* @param {string} [language] Optional language key
* @return {string}
*/
Model.prototype.getTemplateDescription = function ( language ) {
language = language || this.getDefaultLanguage();
return this.description[ language ];
};
/**
* @param {Object|undefined} map New template map info
* @fires change-map
* @fires change
*/
Model.prototype.setMapInfo = function ( map ) {
if ( map !== undefined ) {
if ( !this.constructor.static.compare( this.maps, map ) ) {
if ( this.mapsChanged === false ) {
this.originalMaps = OO.copy( map );
this.mapsChanged = true;
}
this.maps = map;
this.emit( 'change-map', map );
this.emit( 'change' );
}
}
};
/**
* Get the template info.
*
* @return {Object|undefined} The template map info.
*/
Model.prototype.getMapInfo = function () {
return this.maps;
};
/**
* Get the template info.
*
* @return {Object|undefined} The Original template map info.
*/
Model.prototype.getOriginalMapsInfo = function () {
return this.originalMaps;
};
/**
* Get a specific parameter's localized property
*
* @param {string} paramKey Parameter key
* @param {string} property Property name
* @param {string} [language] Optional language key
* @return {string} Parameter property in specified language
*/
Model.prototype.getParamValue = function ( paramKey, property, language ) {
language = language || this.getDefaultLanguage();
return OO.getProp( this.params, paramKey, property, language ) || '';
};
/**
* Get the current wiki language code. Defaults on 'en'.
*
* @return {string} Wiki language
*/
Model.prototype.getDefaultLanguage = function () {
return mw.config.get( 'wgContentLanguage' ) || 'en';
};
/**
* Set template param order array.
*
* @param {string[]} [orderArray] Parameter key array in order
* @fires change-paramOrder
* @fires change
*/
Model.prototype.setTemplateParamOrder = function ( orderArray ) {
orderArray = orderArray || [];
// TODO: Make the compare method verify order of array?
// Copy the array
this.paramOrder = orderArray.slice();
this.emit( 'change-paramOrder', orderArray );
this.emit( 'change' );
};
/**
* Set template format.
*
* @param {string|null} [format=null] Preferred format
* @fires change-format
* @fires change
*/
Model.prototype.setTemplateFormat = function ( format ) {
format = format || null;
if ( this.format !== format ) {
this.format = format;
this.emit( 'change-format', format );
this.emit( 'change' );
}
};
/**
* Add a key to the end of the paramOrder
*
* @param {string} key New key the add into the paramOrder
* @fires add-paramOrder
* @fires change
*/
Model.prototype.addKeyTemplateParamOrder = function ( key ) {
if ( this.paramOrder.indexOf( key ) === -1 ) {
this.paramOrder.push( key );
this.emit( 'add-paramOrder', key );
this.emit( 'change' );
}
};
/**
* TODO: document
*
* @param {string} key
* @param {number} newIndex
* @fires change-paramOrder
* @fires change
*/
Model.prototype.reorderParamOrderKey = function ( key, newIndex ) {
var keyIndex = this.paramOrder.indexOf( key );
// Move the parameter, account for left shift if moving forwards
this.paramOrder.splice(
newIndex - ( newIndex > keyIndex ? 1 : 0 ),
0,
this.paramOrder.splice( keyIndex, 1 )[ 0 ]
);
this.paramOrderChanged = true;
// Emit event
this.emit( 'change-paramOrder', this.paramOrder );
this.emit( 'change' );
};
/**
* Add a key to the end of the paramOrder
*
* @param {string} key New key the add into the paramOrder
* @fires change-paramOrder
* @fires change
*/
Model.prototype.removeKeyTemplateParamOrder = function ( key ) {
var keyPos = this.paramOrder.indexOf( key );
if ( keyPos > -1 ) {
this.paramOrder.splice( keyPos, 1 );
this.emit( 'change-paramOrder', this.paramOrder );
this.emit( 'change' );
}
};
/**
* Retrieve the template paramOrder array
*
* @return {string[]} orderArray Parameter keys in order
*/
Model.prototype.getTemplateParamOrder = function () {
return this.paramOrder;
};
/**
* Retrieve the template preferred format
*
* @return {string|null} Preferred format
*/
Model.prototype.getTemplateFormat = function () {
return this.format;
};
/**
* Set a specific parameter's property
*
* @param {string} paramKey Parameter key
* @param {string} prop Property name
* @param {Mixed} value
* @param {string} [language] Value language
* @return {boolean} Operation was successful
* @fires change-property
* @fires change
*/
Model.prototype.setParamProperty = function ( paramKey, prop, value, language ) {
var allProps = this.constructor.static.getAllProperties( true ),
status = false;
language = language || this.getDefaultLanguage();
if ( !allProps[ prop ] ) {
// The property isn't supported yet
return status;
}
var propertiesWithLanguage = this.constructor.static.getPropertiesWithLanguage();
// Check if the property is split by language code
if ( propertiesWithLanguage.indexOf( prop ) !== -1 ) {
// Initialize property if necessary
if ( !$.isPlainObject( this.params[ paramKey ][ prop ] ) ) {
this.params[ paramKey ][ prop ] = {};
}
value = $.isPlainObject( value ) ? value[ language ] : value;
// Compare with language
if ( !this.constructor.static.compare( this.params[ paramKey ][ prop ][ language ], value ) ) {
this.params[ paramKey ][ prop ][ language ] = value;
this.emit( 'change-property', paramKey, prop, value, language );
this.emit( 'change' );
status = true;
}
} else {
// Compare without language
if ( !this.constructor.static.compare( this.params[ paramKey ][ prop ], value ) ) {
var oldValue = this.params[ paramKey ][ prop ];
this.params[ paramKey ][ prop ] = value;
var newKey;
if ( prop === 'name' && oldValue !== value ) {
newKey = value;
// See if the parameters already has something with this new key
if ( this.params[ newKey ] && !this.params[ newKey ].deleted ) {
// Change the key to be something else
newKey = this.getNewValidParameterKey( newKey );
}
// Copy param details to new name
this.params[ newKey ] = this.params[ paramKey ];
// Delete the old param
this.params[ paramKey ] = { deleted: true };
}
this.emit( 'change-property', paramKey, prop, value, language );
this.emit( 'change' );
if ( prop === 'name' ) {
this.paramOrder[ this.paramOrder.indexOf( paramKey ) ] = newKey || value;
this.paramOrderChanged = true;
this.emit( 'change-paramOrder', this.paramOrder );
this.emit( 'change' );
}
status = true;
}
}
if ( allProps[ prop ].textValue && value === false ) {
// Unset the text value if the boolean it depends on is false
status = this.setParamProperty( paramKey, allProps[ prop ].textValue, '', language );
}
return status;
};
/**
* Mark a parameter for deletion.
* Don't actually delete the parameter so we can make sure it is removed
* from the final output.
*
* @param {string} paramKey Parameter key
* @fires delete-param
* @fires change
*/
Model.prototype.deleteParam = function ( paramKey ) {
this.params[ paramKey ].deleted = true;
// Remove from paramOrder
this.removeKeyTemplateParamOrder( paramKey );
this.emit( 'delete-param', paramKey );
this.emit( 'change' );
};
/**
* Restore parameter by unmarking it as deleted.
*
* @param {string} paramKey Parameter key
* @fires add-param
* @fires change
*/
Model.prototype.restoreParam = function ( paramKey ) {
if ( this.params[ paramKey ] ) {
this.params[ paramKey ].deleted = false;
// Add back to paramOrder
this.addKeyTemplateParamOrder( paramKey );
this.emit( 'add-param', paramKey, this.params[ paramKey ] );
this.emit( 'change' );
}
};
/**
* Delete all data attached to a parameter
*
* @param {string} paramKey Parameter key
*/
Model.prototype.emptyParamData = function ( paramKey ) {
if ( this.params[ paramKey ] ) {
// Delete all data and readd the parameter
delete this.params[ paramKey ];
this.addParam( paramKey );
// Mark this parameter as intentionally emptied
this.params[ paramKey ].emptied = true;
}
};
/**
* Get a parameter property.
*
* @param {string} paramKey Parameter key
* @param {string} prop Parameter property
* @return {Mixed|null} Property value if it exists. Returns null if the
* parameter key itself doesn't exist.
*/
Model.prototype.getParamProperty = function ( paramKey, prop ) {
if ( this.params[ paramKey ] ) {
return this.params[ paramKey ][ prop ];
}
return null;
};
/**
* Retrieve a specific parameter data
*
* @param {string} key Parameter key
* @return {Object|undefined} Parameter data
*/
Model.prototype.getParamData = function ( key ) {
return this.params[ key ];
};
/**
* Return the complete object of all parameters.
*
* @return {Object} All parameters and their data
*/
Model.prototype.getParams = function () {
return this.params;
};
Model.prototype.isParamDeleted = function ( key ) {
return this.params[ key ] && this.params[ key ].deleted === true;
};
Model.prototype.isParamExists = function ( key ) {
return Object.prototype.hasOwnProperty.call( this.params, key );
};
/**
* Set the original templatedata object
*
* @param {Object|null} templatedataObj TemplateData object
*/
Model.prototype.setOriginalTemplateDataObject = function ( templatedataObj ) {
this.originalTemplateDataObject = templatedataObj ? $.extend( true, {}, templatedataObj ) : null;
};
/**
* Get full page name
*
* @param {string} pageName Page name
*/
Model.prototype.setFullPageName = function ( pageName ) {
this.fullPageName = pageName;
};
/**
* Set parent page
*
* @param {string} parent Parent page
*/
Model.prototype.setParentPage = function ( parent ) {
this.parentPage = parent;
};
/**
* Get page full name
*
* @return {string} Page full name
*/
Model.prototype.getFullPageName = function () {
return this.fullPageName;
};
/**
* Get parent page
*
* @return {string} Parent page
*/
Model.prototype.getParentPage = function () {
return this.parentPage;
};
/**
* Get original Parameters/Info from the model and discard any changes
*/
Model.prototype.restoreOriginalMaps = function () {
this.setMapInfo( this.getOriginalMapsInfo() );
};
/**
* Get original templatedata object
*
* @return {Object|null} Templatedata object at the beginning of this editing session, or null
* if we're creating a new object.
*/
Model.prototype.getOriginalTemplateDataObject = function () {
return this.originalTemplateDataObject;
};
/**
* Process the current model and output it
*
* @return {Object} Templatedata object
*/
Model.prototype.outputTemplateData = function () {
var allProps = this.constructor.static.getAllProperties( true ),
original = this.getOriginalTemplateDataObject() || {};
original.params = original.params || {};
var result = $.extend( true, {}, original ),
defaultLang = this.getDefaultLanguage();
var normalizedValue;
// Template description
if ( this.description[ defaultLang ] !== undefined ) {
normalizedValue = this.propRemoveUnusedLanguages( this.description );
if ( this.isOutputInLanguageObject( result.description, normalizedValue ) ) {
result.description = normalizedValue;
} else {
// Store only one language as a string
result.description = normalizedValue[ defaultLang ];
}
} else {
// Delete description
delete result.description;
}
// Template maps
if ( this.maps === undefined || Object.keys( this.maps ).length === 0 ) {
delete result.maps;
} else {
result.maps = this.maps;
}
// Param order
if ( original.paramOrder || this.paramOrderChanged ) {
result.paramOrder = this.paramOrder;
} else {
delete result.paramOrder;
}
// Format
if ( this.format === null ) {
delete result.format;
} else {
result.format = this.format;
}
// Attach sets as-is for now
// TODO: Work properly with sets
if ( original.sets ) {
result.sets = original.sets;
}
// Go over parameters in data
for ( var paramKey in this.params ) {
var key = paramKey;
if ( this.params[ key ].deleted ) {
delete result.params[ key ];
continue;
}
// If the user intentionally empties a parameter, delete it from
// the result and treat it as a new parameter
if ( this.params[ key ].emptied ) {
delete result.params[ key ];
}
// Check if name was changed and change the key accordingly
var name = this.params[ key ].name;
var oldKey = key;
// Notice for clarity:
// Whether the parameter name was changed or not the following
// consistency with object keys will be observed:
// * oldKey: original will use oldKey (for comparison to the old value)
// * key: this.params will use key (for storing without conflict)
// * name: result will use name (for valid output)
// Check if param is new
if ( !result.params[ name ] ) {
// New param. Initialize it
result.params[ name ] = {};
}
// Go over all properties
for ( var prop in allProps ) {
if ( prop === 'deprecatedValue' || prop === 'name' ) {
continue;
}
switch ( allProps[ prop ].type ) {
case 'select':
// Only include type if the original included type
// or if the current type is not undefined
if (
original.params[ key ] &&
original.params[ key ].type !== 'unknown' &&
this.params[ key ].type === 'unknown'
) {
result.params[ name ][ prop ] = undefined;
} else {
result.params[ name ][ prop ] = this.params[ key ].type;
}
break;
case 'boolean':
if ( !this.params[ key ][ prop ] ) {
// Only add a literal false value if there was a false
// value before
if ( original.params[ oldKey ] && original.params[ oldKey ][ prop ] === false ) {
result.params[ name ][ prop ] = false;
} else {
// Otherwise, delete this value
delete result.params[ name ][ prop ];
}
} else {
if ( prop === 'deprecated' ) {
result.params[ name ][ prop ] = this.params[ key ].deprecatedValue || true;
// Remove deprecatedValue
delete result.params[ name ].deprecatedValue;
} else {
result.params[ name ][ prop ] = this.params[ key ][ prop ];
}
}
break;
case 'array':
// Only update these if the new templatedata has an
// array that isn't empty
if (
Array.isArray( this.params[ key ][ prop ] ) &&
this.params[ key ][ prop ].length > 0
) {
result.params[ name ][ prop ] = this.params[ key ][ prop ];
} else {
// If the new array is empty, delete it from the original
delete result.params[ name ][ prop ];
}
break;
default:
// Check if there's a value in the model
if ( this.params[ key ][ prop ] !== undefined ) {
var compareOrig = original.params[ oldKey ] && original.params[ oldKey ][ prop ];
if ( allProps[ prop ].allowLanguages ) {
normalizedValue = this.propRemoveUnusedLanguages( this.params[ key ][ prop ] );
// Check if this should be displayed with language object or directly as string
if ( this.isOutputInLanguageObject( compareOrig || {}, normalizedValue ) ) {
result.params[ name ][ prop ] = normalizedValue;
} else {
// Store only one language as a string
result.params[ name ][ prop ] = normalizedValue[ defaultLang ];
}
} else if ( this.params[ key ][ prop ] ||
// Add empty strings only if the property existed before (empty or not)
compareOrig !== undefined
) {
// Set up the result
result.params[ name ][ prop ] = this.params[ key ][ prop ];
}
}
break;
}
}
}
return result;
};
/**
* Check the key if it already exists in the parameter list. If it does,
* find a new key that doesn't, and return it.
*
* @param {string} key New parameter key
* @return {string} Valid new parameter key
*/
Model.prototype.getNewValidParameterKey = function ( key ) {
var allParamNames = this.getAllParamNames();
if ( this.params[ key ] || allParamNames.indexOf( key ) !== -1 ) {
// Change the key to be something else
if ( /\d$/.test( key ) ) {
key += '-';
}
key += this.paramIdentifierCounter;
this.paramIdentifierCounter++;
return this.getNewValidParameterKey( key );
} else {
return key;
}
};
/**
* Go over a language property and remove empty language key values
*
* @param {Object} propData Property data
* @return {Object} Property data with only used language keys
*/
Model.prototype.propRemoveUnusedLanguages = function ( propData ) {
var result = {};
if ( $.isPlainObject( propData ) ) {
for ( var key in propData ) {
if ( propData[ key ] ) {
result[ key ] = propData[ key ];
}
}
}
return result;
};
/**
* Check whether the output of the current parameter property should be
* outputted in full language mode (object) or a simple string.
*
* @param {string|Object} originalPropValue Original property value
* @param {string|Object} newPropValue New property value
* @return {boolean} Output should be a full language object
*/
Model.prototype.isOutputInLanguageObject = function ( originalPropValue, newPropValue ) {
if (
(
// The original was already split to languages
typeof originalPropValue === 'object' &&
// Original was not an empty object
!$.isEmptyObject( originalPropValue )
) ||
(
// The new value is split to languages
typeof newPropValue === 'object' &&
// New object is not empty
!$.isEmptyObject( newPropValue ) &&
(
// The new value doesn't have the default language
newPropValue[ this.getDefaultLanguage() ] === undefined ||
// There is more than just one language in the new property
Object.keys( newPropValue ).length > 1
)
)
) {
return true;
}
return false;
};
/**
* Set the parameters that are available in the template source code
*
* @param {string[]} sourceParams Parameters available in template source
*/
Model.prototype.setSourceCodeParameters = function ( sourceParams ) {
this.sourceCodeParameters = sourceParams;
};
/**
* Get the parameters that are available in the template source code
*
* @return {string[]} Parameters available in template source
*/
Model.prototype.getSourceCodeParameters = function () {
return this.sourceCodeParameters;
};
module.exports = Model;
modules/ext.templateDataGenerator.editTemplatePage/templateDataGenerator.placeholder.less 0000666 00000000467 15133472347 0026155 0 ustar 00 .client-js .tdg-editscreen-placeholder::after {
content: '\00a0';
display: block;
line-height: 1.42857143em; /* Mirroring the button's line-height */
padding-top: ( 5px + 1px ) * 2; /* Mirroring the button's 5px padding + 1px border */
margin-bottom: 1em; /* Mirroring the .tdg-editscreen-main margin */
}
modules/ext.templateDataGenerator.editTemplatePage/widgets/LanguageSearchWidget.js 0000666 00000006545 15133472347 0024554 0 ustar 00 var LanguageResultWidget = require( './LanguageResultWidget.js' );
/**
* Creates a TemplateDataLanguageSearchWidget object.
* This is a copy of ve.ui.LanguageSearchWidget.
*
* @class
* @extends OO.ui.SearchWidget
*
* @constructor
* @param {Object} [config] Configuration options
*/
function LanguageSearchWidget( config ) {
// Configuration initialization
config = $.extend( {
placeholder: mw.msg( 'templatedata-modal-search-input-placeholder' )
}, config );
// Parent constructor
LanguageSearchWidget.parent.call( this, config );
// Properties
this.filteredLanguageResultWidgets = [];
var languageCodes = Object.keys( $.uls.data.getAutonyms() ).sort();
this.languageResultWidgets = languageCodes.map( function ( languageCode ) {
return new LanguageResultWidget( {
data: {
code: languageCode,
name: $.uls.data.getAutonym( languageCode ),
autonym: $.uls.data.getAutonym( languageCode )
}
} );
} );
this.setAvailableLanguages();
// Initialization
this.$element.addClass( 'tdg-languageSearchWidget' );
}
/* Inheritance */
OO.inheritClass( LanguageSearchWidget, OO.ui.SearchWidget );
/* Methods */
/**
* FIXME: this should be inheritdoc
*/
LanguageSearchWidget.prototype.onQueryChange = function () {
// Parent method
LanguageSearchWidget.parent.prototype.onQueryChange.apply( this, arguments );
// Populate
this.addResults();
};
/**
* Set available languages to show
*
* @param {string[]} [availableLanguages] Available language codes to show, all if undefined
*/
LanguageSearchWidget.prototype.setAvailableLanguages = function ( availableLanguages ) {
if ( !availableLanguages ) {
this.filteredLanguageResultWidgets = this.languageResultWidgets.slice();
return;
}
this.filteredLanguageResultWidgets = this.languageResultWidgets.map( function ( languageResult ) {
var data = languageResult.getData();
if ( availableLanguages.indexOf( data.code ) !== -1 ) {
return languageResult;
}
return null;
} ).filter( function ( languageResult ) {
return languageResult;
} );
};
/**
* Update search results from current query
*/
LanguageSearchWidget.prototype.addResults = function () {
var matchProperties = [ 'name', 'autonym', 'code' ],
query = this.query.getValue().trim(),
compare = window.Intl && Intl.Collator ?
new Intl.Collator( this.lang, { sensitivity: 'base' } ).compare :
function ( a, b ) { return a.toLowerCase() === b.toLowerCase() ? 0 : 1; },
hasQuery = !!query.length,
items = [];
var results = this.getResults();
results.clearItems();
this.filteredLanguageResultWidgets.forEach( function ( languageResult ) {
var data = languageResult.getData();
var matchedProperty = null;
matchProperties.some( function ( prop ) {
if ( data[ prop ] && compare( data[ prop ].slice( 0, query.length ), query ) === 0 ) {
matchedProperty = prop;
return true;
}
return false;
} );
if ( query === '' || matchedProperty ) {
items.push(
languageResult
.updateLabel( query, matchedProperty, compare )
.setSelected( false )
.setHighlighted( false )
// Forward keyboard-triggered events from the OptionWidget to the SelectWidget
.off( 'choose' )
.connect( results, { choose: [ 'emit', 'choose' ] } )
);
}
} );
results.addItems( items );
if ( hasQuery ) {
results.highlightItem( results.findFirstSelectableItem() );
}
};
module.exports = LanguageSearchWidget;
modules/ext.templateDataGenerator.editTemplatePage/widgets/AutosizeTextInputWidget.js 0000666 00000001470 15133472347 0025363 0 ustar 00 /**
* Creates a AutosizeTextInputWidget object.
* Used to allow autosizable text input to handle bigger content in the template data editor.
*
* @class
* @extends OO.ui.MultilineTextInputWidget
*
* @constructor
* @param {Object} config
*/
function AutosizeTextInputWidget( config ) {
config.autosize = true;
config.rows = 1;
// Parent constructor
AutosizeTextInputWidget.super.call( this, config );
}
/* Inheritance */
OO.inheritClass( AutosizeTextInputWidget, OO.ui.MultilineTextInputWidget );
/* Methods */
/**
* @inheritdoc
*/
AutosizeTextInputWidget.prototype.onKeyPress = function ( e ) {
if ( e.which === OO.ui.Keys.ENTER ) {
// block adding of newlines
e.preventDefault();
}
OO.ui.MultilineTextInputWidget.prototype.onKeyPress.call( this, e );
};
module.exports = AutosizeTextInputWidget;
modules/ext.templateDataGenerator.editTemplatePage/widgets/ParamSelectWidget.js 0000666 00000001665 15133472347 0024101 0 ustar 00 /**
* TemplateData parameter select widget
*
* @class
* @extends OO.ui.SelectWidget
* @mixes OO.ui.mixin.DraggableGroupElement
*
* @param {Object} [config] Dialog configuration object
*/
function ParamSelectWidget( config ) {
// Parent constructor
ParamSelectWidget.parent.call( this, config );
// Mixin constructors
OO.ui.mixin.DraggableGroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
// Initialize
this.$element.addClass( 'tdg-templateDataParamSelectWidget' );
}
/* Inheritance */
OO.inheritClass( ParamSelectWidget, OO.ui.SelectWidget );
OO.mixinClass( ParamSelectWidget, OO.ui.mixin.DraggableGroupElement );
ParamSelectWidget.prototype.onMouseDown = function ( e ) {
if ( $( e.target ).closest( '.oo-ui-draggableElement-handle' ).length || e.shiftKey ) {
return true;
}
return ParamSelectWidget.parent.prototype.onMouseDown.apply( this, arguments );
};
module.exports = ParamSelectWidget;
modules/ext.templateDataGenerator.editTemplatePage/widgets/ParamWidget.js 0000666 00000004034 15133472347 0022732 0 ustar 00 /**
* TemplateData Param Widget
*
* @class
* @extends OO.ui.DecoratedOptionWidget
* @mixes OO.ui.mixin.DraggableElement
*
* @param {Object} data Parameter data
* @param {Object} [config] Configuration object
*/
function ParamWidget( data, config ) {
config = config || {};
// Parent constructor
ParamWidget.parent.call( this, $.extend( {}, config, { data: data.key, icon: 'menu' } ) );
// Mixin constructors
OO.ui.mixin.DraggableElement.call( this, $.extend( { $handle: this.$icon } ) );
OO.ui.mixin.TabIndexedElement.call( this, { $tabIndexed: this.$element } );
this.key = data.key;
this.label = data.label;
this.aliases = data.aliases || [];
this.description = data.description;
// Events
this.$element.on( 'keydown', this.onKeyDown.bind( this ) );
// Initialize
this.$element.addClass( 'tdg-templateDataParamWidget' );
this.buildParamLabel();
}
/* Inheritance */
OO.inheritClass( ParamWidget, OO.ui.DecoratedOptionWidget );
OO.mixinClass( ParamWidget, OO.ui.mixin.DraggableElement );
OO.mixinClass( ParamWidget, OO.ui.mixin.TabIndexedElement );
/**
* @param {jQuery.Event} e Key down event
* @fires choose
*/
ParamWidget.prototype.onKeyDown = function ( e ) {
if ( e.which === OO.ui.Keys.ENTER ) {
this.emit( 'choose', this );
}
};
/**
* Build the parameter label in the parameter select widget
*/
ParamWidget.prototype.buildParamLabel = function () {
var keys = this.aliases.slice(),
$paramLabel = $( '<div>' )
.addClass( 'tdg-templateDataParamWidget-param-name' ),
$aliases = $( '<div>' )
.addClass( 'tdg-templateDataParamWidget-param-aliases' ),
$description = $( '<div>' )
.addClass( 'tdg-templateDataParamWidget-param-description' );
keys.unshift( this.key );
$paramLabel.text( this.label || this.key );
$description.text( this.description );
keys.forEach( function ( key ) {
$aliases.append(
$( '<span>' )
.addClass( 'tdg-templateDataParamWidget-param-alias' )
.text( key )
);
} );
this.setLabel( $aliases.add( $paramLabel ).add( $description ) );
};
module.exports = ParamWidget;
modules/ext.templateDataGenerator.editTemplatePage/widgets/ParamImportWidget.js 0000666 00000002226 15133472347 0024126 0 ustar 00 /**
* TemplateData Param Import Widget
*
* @class
* @extends OO.ui.ButtonWidget
* @param {Object} [config]
*/
function ParamImportWidget( config ) {
config = config || {};
// Parent constructor
ParamImportWidget.parent.call( this, $.extend( {
icon: 'parameter-set'
}, config ) );
// Initialize
this.$element.addClass( 'tdg-templateDataParamImportWidget' );
}
/* Inheritance */
OO.inheritClass( ParamImportWidget, OO.ui.ButtonWidget );
/**
* Build the parameter label in the parameter select widget
*
* @param {string[]} params Param names
*/
ParamImportWidget.prototype.buildParamLabel = function ( params ) {
var paramNames = params.slice( 0, 9 ).join( mw.msg( 'comma-separator' ) ),
$paramCount = $( '<div>' )
.addClass( 'tdg-templateDataParamWidget-param-name' ),
$paramNames = $( '<div>' )
.addClass( 'tdg-templateDataParamWidget-param-description' );
$paramCount.text( mw.msg( 'templatedata-modal-table-param-importoption', params.length ) );
$paramNames.text( mw.msg( 'templatedata-modal-table-param-importoption-subtitle', paramNames ) );
this.setLabel( $paramCount.add( $paramNames ) );
};
module.exports = ParamImportWidget;
modules/ext.templateDataGenerator.editTemplatePage/widgets/LanguageResultWidget.js 0000666 00000005213 15133472347 0024614 0 ustar 00 /**
* Creates a LanguageResultWidget object.
* This is a copy of ve.ui.LanguageResultWidget
*
* @class
* @extends OO.ui.OptionWidget
*
* @constructor
* @param {Object} [config] Configuration options
*/
function LanguageResultWidget( config ) {
// Parent constructor
LanguageResultWidget.parent.call( this, config );
// Mixin constructors
OO.ui.mixin.TabIndexedElement.call( this );
// Events
this.$element.on( 'keydown', this.onKeyDown.bind( this ) );
// Initialization
this.$element.addClass( 'tdg-languageResultWidget' );
this.$name = $( '<div>' ).addClass( 'tdg-languageResultWidget-name' );
this.$otherMatch = $( '<div>' ).addClass( 'tdg-languageResultWidget-otherMatch' );
this.setLabel( this.$otherMatch.add( this.$name ) );
}
/* Inheritance */
OO.inheritClass( LanguageResultWidget, OO.ui.OptionWidget );
OO.mixinClass( LanguageResultWidget, OO.ui.mixin.TabIndexedElement );
/* Methods */
/**
* @param {jQuery.Event} e Key down event
* @fires choose
*/
LanguageResultWidget.prototype.onKeyDown = function ( e ) {
if ( e.which === OO.ui.Keys.ENTER ) {
this.emit( 'choose', this );
}
};
/**
* Update labels based on query
*
* @param {string} [query] Query text which matched this result
* @param {string} [matchedProperty] Data property which matched the query text
* @return {LanguageResultWidget}
* @chainable
*/
LanguageResultWidget.prototype.updateLabel = function ( query, matchedProperty ) {
var data = this.getData();
// Reset text
this.$name.text( data.name );
this.$otherMatch.text( data.code );
// Highlight where applicable
if ( matchedProperty ) {
var $highlighted = this.constructor.static.highlightQuery( data[ matchedProperty ], query );
if ( matchedProperty === 'name' ) {
this.$name.empty().append( $highlighted );
} else {
this.$otherMatch.empty().append( $highlighted );
}
}
return this;
};
/**
* Highlight text where a substring query matches
*
* Copied from ve#highlightQuery
*
* @param {string} text Text
* @param {string} query Query to find
* @return {jQuery} Text with query substring wrapped in highlighted span
*/
LanguageResultWidget.static.highlightQuery = function ( text, query ) {
var $result = $( '<span>' ),
offset = text.toLowerCase().indexOf( query.toLowerCase() );
if ( !query.length || offset === -1 ) {
return $result.text( text );
}
$result.append(
document.createTextNode( text.slice( 0, offset ) ),
$( '<span>' )
.addClass( 'tdg-languageResultWidget-highlight' )
.text( text.slice( offset, offset + query.length ) ),
document.createTextNode( text.slice( offset + query.length ) )
);
return $result.contents();
};
module.exports = LanguageResultWidget;
modules/ext.templateDataGenerator.editTemplatePage/Target.js 0000666 00000020620 15133472347 0020305 0 ustar 00 var Dialog = require( './Dialog.js' ),
DataModule = require( 'ext.templateDataGenerator.data' ),
Model = DataModule.Model,
SourceHandler = DataModule.SourceHandler;
/**
* Template data edit ui target
*
* @class
* @extends OO.ui.Element
* @mixes OO.EventEmitter
*
* @constructor
* @param {jQuery} $textarea Editor textarea
* @param {Object} config
*/
function Target( $textarea, config ) {
var target = this;
// Parent constructor
Target.super.call( this, config );
// Mixin constructor
OO.EventEmitter.call( this );
this.pageName = config.pageName;
this.parentPage = config.parentPage;
this.isPageSubLevel = !!config.isPageSubLevel;
this.isDocPage = !!config.isDocPage;
this.docSubpage = config.docSubpage;
this.$textarea = $textarea;
this.editOpenDialogButton = new OO.ui.ButtonWidget( {
label: mw.msg( 'templatedata-editbutton' )
} );
this.editNoticeMessage = new OO.ui.MessageWidget( {
classes: [ 'tdg-editscreen-edit-notice' ]
} )
.toggle( false );
var $helpLink = $( '<a>' )
.attr( {
href: mw.msg( 'templatedata-helplink-target' ),
target: '_blank'
} )
.addClass( 'tdg-editscreen-main-helplink' )
.text( mw.msg( 'templatedata-helplink' ) );
this.windowManager = new OO.ui.WindowManager();
// Ensure OOUI's window manager is on top of ours, because we use OO.ui.confirm() elsewhere.
// This is a bit silly...
OO.ui.getWindowManager().$element.before( this.windowManager.$element );
// Dialog
this.tdgDialog = new Dialog( config );
this.windowManager.addWindows( [ this.tdgDialog ] );
this.sourceHandler = new SourceHandler( {
fullPageName: this.pageName,
parentPage: this.parentPage,
isPageSubLevel: this.isPageSubLevel
} );
// Check if there's already a templatedata in a related page
var relatedPage = this.isDocPage ? this.parentPage : this.pageName + '/' + this.docSubpage;
this.sourceHandler.getApi( relatedPage )
.then( function ( result ) {
var response = result.query.pages[ result.query.pageids[ 0 ] ];
// HACK: When checking whether a related page (parent for /doc page or
// vice versa) already has a templatedata string, we shouldn't
// ask for the 'templatedata' action but rather the actual content
// of the related page, otherwise we get embedded templatedata and
// wrong information is presented.
if ( response.missing === undefined ) {
var content = response.revisions[ 0 ][ '*' ];
// There's a templatedata string
if ( content.match( /<templatedata>/i ) ) {
// HACK: Setting a link in the messages doesn't work. The bug report offers
// a somewhat hacky work around that includes setting a separate message
// to be parsed.
// https://phabricator.wikimedia.org/T49395#490610
var msg = mw.message( 'templatedata-exists-on-related-page', relatedPage ).plain();
mw.messages.set( { 'templatedata-string-exists-hack-message': msg } );
msg = new OO.ui.HtmlSnippet(
mw.message( 'templatedata-string-exists-hack-message' ).parse()
);
target.setEditNoticeMessage( msg, 'warning' );
}
}
} );
// Events
this.editOpenDialogButton.connect( this, { click: 'onEditOpenDialogButton' } );
this.tdgDialog.connect( this, { apply: 'onDialogApply' } );
this.$element
.addClass( 'tdg-editscreen-main' )
.append(
this.editOpenDialogButton.$element,
$helpLink,
this.editNoticeMessage.$element
);
}
/* Inheritance */
OO.inheritClass( Target, OO.ui.Element );
OO.mixinClass( Target, OO.EventEmitter );
/* Methods */
/**
* Destroy the target
*/
Target.prototype.destroy = function () {
this.windowManager.destroy();
this.$element.remove();
};
/**
* Display error message in the edit window
*
* @method setNoticeMessage
* @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Message to display
* @param {string} type Message type 'notice', 'error', 'warning' or 'success'
*/
Target.prototype.setEditNoticeMessage = function ( label, type ) {
this.editNoticeMessage.setLabel( label );
this.editNoticeMessage.setType( type );
this.editNoticeMessage.toggle( true );
};
/**
* Open the templatedata edit dialog
*
* @method openEditDialog
* @param {Model} dataModel The data model
* associated with this edit dialog.
*/
Target.prototype.openEditDialog = function ( dataModel ) {
// Open the edit dialog
this.windowManager.openWindow( 'TemplateDataDialog', {
model: dataModel,
editNoticeMessage: this.editNoticeMessage
} );
};
/**
* Respond to edit dialog button click.
*
* @method onEditOpenDialogButton
*/
Target.prototype.onEditOpenDialogButton = function () {
var target = this;
this.originalWikitext = this.$textarea.textSelection( 'getContents' );
// Build the model
this.sourceHandler.buildModel( this.originalWikitext )
.then(
// Success
function ( model ) {
target.openEditDialog( model );
},
// Failure
function () {
// Open a message dialog
OO.ui.getWindowManager().openWindow( 'message', {
title: mw.msg( 'templatedata-modal-title' ),
message: mw.msg( 'templatedata-errormsg-jsonbadformat' ),
verbose: true,
actions: [
{
action: 'accept',
label: mw.msg( 'templatedata-modal-json-error-replace' ),
flags: [ 'primary', 'destructive' ]
},
{
action: 'reject',
label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ),
flags: 'safe'
}
]
} ).closed.then( function ( data ) {
if ( data && data.action === 'accept' ) {
// Open the dialog with an empty model
var model = Model.static.newFromObject(
null,
target.sourceHandler.getTemplateSourceCodeParams()
);
target.openEditDialog( model );
}
} );
}
);
};
/**
* Replace the old templatedata string with the new one, or
* insert the new one into the page if an old one doesn't exist
*
* @method replaceTemplateData
* @param {Object} newTemplateData New templatedata
*/
Target.prototype.replaceTemplateData = function ( newTemplateData ) {
var templateDataJSON = JSON.stringify( newTemplateData, null, '\t' ),
templatedataPattern = /(<templatedata>\s*)([\s\S]*?)\s*<\/templatedata>/i;
var matches, templateDataOutput;
if ( ( matches = this.originalWikitext.match( templatedataPattern ) ) ) {
// Move cursor to select withing existing <templatedata> and whitespace
this.$textarea.textSelection( 'setSelection', {
start: matches.index + matches[ 1 ].length,
end: matches.index + matches[ 1 ].length + matches[ 2 ].length
} );
templateDataOutput = templateDataJSON;
} else {
this.$textarea.textSelection( 'setSelection', { start: this.originalWikitext.length } );
templateDataOutput = '<templatedata>\n' + templateDataJSON + '\n</templatedata>';
if ( !this.isPageSubLevel ) {
if ( ( matches = this.originalWikitext.match( /<\/noinclude>\s*$/ ) ) ) {
// Move cursor inside </noinclude>
this.$textarea.textSelection( 'setSelection', { start: matches.index } );
} else {
// Wrap in new <noinclude>s
templateDataOutput = '<noinclude>\n' + templateDataOutput + '\n</noinclude>\n';
}
}
if ( this.originalWikitext.slice( -1 ) !== '\n' ) {
templateDataOutput = '\n' + templateDataOutput;
}
}
this.$textarea.textSelection( 'replaceSelection', templateDataOutput );
};
/**
* Respond to edit dialog apply event
*
* @method onDialogApply
* @param {Object} templateData New templatedata
*/
Target.prototype.onDialogApply = function ( templateData ) {
var target = this;
if (
Object.keys( templateData ).length > 1 ||
Object.keys( templateData.params ).length > 0
) {
this.replaceTemplateData( templateData );
} else {
this.windowManager.closeWindow( this.windowManager.getCurrentWindow() );
OO.ui.getWindowManager().openWindow( 'message', {
title: mw.msg( 'templatedata-modal-title' ),
message: mw.msg( 'templatedata-errormsg-insertblank' ),
actions: [
{
label: mw.msg( 'templatedata-modal-button-cancel' ),
flags: [ 'primary', 'safe' ]
},
{
action: 'apply',
label: mw.msg( 'templatedata-modal-button-apply' )
}
]
} ).closed.then( function ( data ) {
if ( data && data.action === 'apply' ) {
target.replaceTemplateData( templateData );
}
} );
}
// TODO: Remove when not needed any more, see T267926
// eslint-disable-next-line no-jquery/no-global-selector
if ( !$( 'input[name="TemplateDataGeneratorUsed"]' ).length ) {
$( '<input>' ).attr( {
type: 'hidden',
value: 1,
name: 'TemplateDataGeneratorUsed'
} ).insertAfter( '#wpTextbox1' );
}
};
module.exports = Target;
modules/ext.templateDataGenerator.editTemplatePage/templateDataGenerator.ui.less 0000666 00000003366 15133472347 0024311 0 ustar 00 .tdg-editscreen-main {
margin-bottom: 1em;
.skin-vector-legacy .ve-init-mw-desktopArticleTarget-originalContent & {
font-size: 14/16em;
}
}
.tdg-editscreen-main-helplink {
margin-left: 1em;
}
.tdg-editscreen-edit-notice {
margin-top: 0.5em;
}
.tdg-templateDataDialog-panels .oo-ui-messageWidget p {
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
.tdg-editscreen-input-error .oo-ui-inputWidget-input {
background-color: #ffa3a3;
}
.tdg-templateDataParamWidget {
padding-right: 0.5em;
.oo-ui-iconElement-icon {
opacity: 0.2;
}
/* Hover events sometimes fire while dragging, so limit to highlighted widgets */
&.oo-ui-optionWidget-highlighted .oo-ui-iconElement-icon:hover {
opacity: 0.5;
}
}
.tdg-templateDataParamWidget-param {
&-name,
&-aliases,
&-description {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&-name {
font-weight: bold;
}
&-aliases {
float: right;
font-size: 0.9em;
padding: 1px;
}
&-description {
color: #666;
}
&-alias {
border-radius: 0.3em;
padding: 0.125em 0.25em;
margin-left: 0.5em;
color: #999;
background-color: #eee;
border: solid 1px #ddd;
&:first-child {
background-color: #fff;
}
}
}
.tdg-templateDataParamWidget-paramList-param-description {
clear: both;
color: #555;
}
.tdg-templateDataParamImportWidget {
display: block;
margin-top: 1em;
> .oo-ui-buttonElement-button {
display: block;
text-align: left;
font-weight: normal;
}
}
/* LanguageSearchWidget */
.tdg-languageResultWidget-otherMatch {
float: right;
color: #777;
}
.tdg-languageResultWidget-highlight {
font-weight: bold;
}
.mw-templateData-template-add-map-button .oo-ui-buttonElement-button {
text-align: left;
white-space: normal;
}
modules/ext.templateDataGenerator.editTemplatePage/init.js 0000666 00000004073 15133472347 0020026 0 ustar 00 /*!
* TemplateData Generator edit template page init
*
* @author Moriel Schottlender
* @author Ed Sanders
*/
/* global ve */
/* eslint-disable no-jquery/no-global-selector */
'use strict';
new mw.Api().loadMessages( 'templatedata-doc-subpage', { amlang: mw.config.get( 'wgContentLanguage' ) } ).then( function () {
var Target = require( './Target.js' ),
pageName = mw.config.get( 'wgPageName' ),
docSubpage = mw.msg( 'templatedata-doc-subpage' ),
config = {
pageName: pageName,
isPageSubLevel: false
},
$textbox = $( '#wpTextbox1' );
var pieces = pageName.split( '/' );
var isDocPage = pieces.length > 1 && pieces[ pieces.length - 1 ] === docSubpage;
var openTDG = new URL( location.href ).searchParams.get( 'templatedata' ) === 'edit';
config = {
pageName: pageName,
isPageSubLevel: pieces.length > 1,
parentPage: pageName,
isDocPage: isDocPage,
docSubpage: docSubpage
};
// Only if we are in a doc page do we set the parent page to
// the one above. Otherwise, all parent pages are current pages
if ( isDocPage ) {
pieces.pop();
config.parentPage = pieces.join( '/' );
}
// Textbox wikitext editor
if ( $textbox.length ) {
// Prepare the editor
var wtTarget = new Target( $textbox, config );
$( '.tdg-editscreen-placeholder' ).replaceWith( wtTarget.$element );
if ( openTDG ) {
wtTarget.onEditOpenDialogButton();
}
}
var veTarget;
// Visual editor source mode
mw.hook( 've.activationComplete' ).add( function () {
var surface = ve.init.target.getSurface();
if ( surface.getMode() === 'source' ) {
// Source mode will have created a dummy textbox
$textbox = $( '#wpTextbox1' );
veTarget = new Target( $textbox, config );
// Use the same font size as main content text
veTarget.$element.addClass( 'mw-body-content' );
$( '.ve-init-mw-desktopArticleTarget-originalContent' ).prepend( veTarget.$element );
if ( openTDG ) {
veTarget.onEditOpenDialogButton();
}
}
} );
mw.hook( 've.deactivationComplete' ).add( function () {
if ( veTarget ) {
veTarget.destroy();
veTarget = null;
}
} );
} );
modules/ext.templateDataGenerator.editTemplatePage/Metrics.js 0000666 00000001250 15133472347 0020463 0 ustar 00 function logEvent( eventName ) {
/* eslint-disable camelcase */
var event = {
action: eventName,
page_id: mw.config.get( 'wgArticleId' ),
page_title: mw.config.get( 'wgTitle' ),
page_namespace: mw.config.get( 'wgNamespaceNumber' ),
rev_id: mw.config.get( 'wgCurRevisionId' ),
user_edit_count: mw.config.get( 'wgUserEditCount', 0 ),
user_id: mw.user.isNamed() ? mw.user.getId() : 0
};
var editCountBucket = mw.config.get( 'wgUserEditCountBucket' );
if ( editCountBucket !== null ) {
event.user_edit_count_bucket = editCountBucket;
}
/* eslint-enable camelcase */
mw.track( 'event.TemplateDataEditor', event );
}
module.exports = {
logEvent: logEvent
};
modules/ext.templateDataGenerator.editTemplatePage/Dialog.js 0000666 00000143306 15133472347 0020265 0 ustar 00 var AutosizeTextInputWidget = require( './widgets/AutosizeTextInputWidget.js' ),
LanguageSearchWidget = require( './widgets/LanguageSearchWidget.js' ),
Metrics = require( './Metrics.js' ),
Model = require( 'ext.templateDataGenerator.data' ).Model,
ParamImportWidget = require( './widgets/ParamImportWidget.js' ),
ParamSelectWidget = require( './widgets/ParamSelectWidget.js' ),
ParamWidget = require( './widgets/ParamWidget.js' );
/**
* TemplateData Dialog
*
* @class
* @extends OO.ui.ProcessDialog
*
* @constructor
* @param {Object} config Dialog configuration object
*
* @external LanguageResultWidget
*/
function Dialog( config ) {
// Parent constructor
Dialog.parent.call( this, config );
this.model = null;
this.modified = false;
this.language = null;
this.availableLanguages = [];
this.selectedParamKey = '';
this.propInputs = {};
this.propFieldLayout = {};
this.isSetup = false;
this.mapsCache = undefined;
this.descriptionChanged = false;
this.paramsReordered = false;
this.paramPropertyChangeTracking = {};
// Initialize
this.$element.addClass( 'tdg-templateDataDialog' );
}
/* Inheritance */
OO.inheritClass( Dialog, OO.ui.ProcessDialog );
/* Static properties */
Dialog.static.name = 'TemplateDataDialog';
Dialog.static.title = mw.msg( 'templatedata-modal-title' );
Dialog.static.size = 'large';
Dialog.static.actions = [
{
action: 'apply',
label: mw.msg( 'templatedata-modal-button-apply' ),
flags: [ 'primary', 'progressive' ],
modes: 'list'
},
{
action: 'done',
label: mw.msg( 'templatedata-modal-button-done' ),
flags: [ 'primary', 'progressive' ],
modes: [ 'edit', 'maps' ]
},
{
action: 'add',
label: mw.msg( 'templatedata-modal-button-addparam' ),
icon: 'add',
flags: [ 'progressive' ],
modes: 'list'
},
{
action: 'delete',
label: mw.msg( 'templatedata-modal-button-delparam' ),
modes: 'edit',
flags: 'destructive'
},
{
action: 'cancel',
label: mw.msg( 'templatedata-modal-button-cancel' ),
modes: 'maps',
flags: 'destructive'
},
{
label: mw.msg( 'templatedata-modal-button-cancel' ),
flags: [ 'safe', 'close' ],
modes: [ 'list', 'error' ]
},
{
action: 'back',
label: mw.msg( 'templatedata-modal-button-back' ),
flags: [ 'safe', 'back' ],
modes: [ 'language', 'add' ]
}
];
/**
* Initialize window contents.
*
* The first time the window is opened, #initialize is called so that changes to the window that
* will persist between openings can be made. See #getSetupProcess for a way to make changes each
* time the window opens.
*
* @throws {Error} If not attached to a manager
* @chainable
*/
Dialog.prototype.initialize = function () {
// Parent method
Dialog.super.prototype.initialize.call( this );
this.$spinner = $( '<div>' ).addClass( 'tdg-spinner' ).text( 'working...' );
this.$body.append( this.$spinner );
this.noticeMessage = new OO.ui.MessageWidget();
this.noticeMessage.toggle( false );
this.panels = new OO.ui.StackLayout( { continuous: false } );
this.listParamsPanel = new OO.ui.PanelLayout( { padded: true, scrollable: true } );
this.editParamPanel = new OO.ui.PanelLayout( { padded: true } );
this.languagePanel = new OO.ui.PanelLayout();
this.addParamPanel = new OO.ui.PanelLayout( { padded: true } );
this.editMapsPanel = new OO.ui.PanelLayout();
// Language panel
this.newLanguageSearch = new LanguageSearchWidget();
// Add parameter panel
this.newParamInput = new OO.ui.TextInputWidget( {
placeholder: mw.msg( 'templatedata-modal-placeholder-paramkey' )
} );
this.addParamButton = new OO.ui.ButtonWidget( {
label: mw.msg( 'templatedata-modal-button-addparam' ),
flags: [ 'progressive', 'primary' ],
disabled: true
} );
var addParamFieldlayout = new OO.ui.ActionFieldLayout(
this.newParamInput,
this.addParamButton,
{
align: 'top',
label: mw.msg( 'templatedata-modal-title-addparam' )
}
);
// Maps panel
this.templateMapsInput = new OO.ui.MultilineTextInputWidget( {
classes: [ 'mw-templateData-template-maps-input' ],
autosize: true,
rows: this.getBodyHeight() / 22.5,
maxRows: this.getBodyHeight() / 22.5,
placeholder: mw.msg( 'templatedata-modal-placeholder-mapinfo' ),
scrollable: true
} );
this.removeMapButton = new OO.ui.ButtonWidget( {
classes: [ 'mw-templateData-template-remove-map-button' ],
label: mw.msg( 'templatedata-modal-button-removemap' ),
icon: 'trash',
flags: [ 'destructive' ]
} );
this.addNewMapButton = new OO.ui.ButtonWidget( {
classes: [ 'mw-templateData-template-add-map-button' ],
label: mw.msg( 'templatedata-modal-button-addmap' ),
icon: 'add',
framed: false,
flags: [ 'progressive' ]
} );
this.newMapNameInput = new OO.ui.TextInputWidget( {
value: '',
placeholder: mw.msg( 'templatedata-modal-placeholder-prompt-map-name' ),
classes: [ 'mw-templateData-template-map-prompter' ]
} );
this.cancelAddMapButton = new OO.ui.ButtonWidget( {
label: mw.msg( 'templatedata-modal-button-cancel' ),
framed: false,
flags: [ 'destructive' ]
} );
this.saveAddMapButton = new OO.ui.ButtonWidget( {
label: mw.msg( 'templatedata-modal-button-done' ),
framed: false,
flags: [ 'primary', 'progressive' ]
} );
this.mapsGroup = new OO.ui.OutlineSelectWidget( {
classes: [ 'mw-templateData-template-map-group' ]
} );
var addNewMapButtonPanel = new OO.ui.PanelLayout( {
classes: [ 'mw-templateData-template-add-map-button-panel' ],
padded: true,
expanded: true
} );
var mapsListPanel = new OO.ui.PanelLayout( {
expanded: true,
scrollable: true
} );
var mapsListMenuLayout = new OO.ui.MenuLayout( {
classes: [ 'mw-templateData-template-map-list-menu-panel' ],
menuPosition: 'top',
expanded: true,
contentPanel: mapsListPanel,
menuPanel: addNewMapButtonPanel
} );
var mapsContentPanel = new OO.ui.PanelLayout( {
padded: true,
expanded: true
} );
var templateMapsMenuLayout = new OO.ui.MenuLayout( {
contentPanel: mapsContentPanel,
menuPanel: mapsListMenuLayout
} );
// Param list panel (main)
this.languageDropdownWidget = new OO.ui.DropdownWidget();
this.languagePanelButton = new OO.ui.ButtonWidget( {
label: mw.msg( 'templatedata-modal-button-add-language' ),
flags: [ 'progressive' ]
} );
var languageActionFieldLayout = new OO.ui.ActionFieldLayout(
this.languageDropdownWidget,
this.languagePanelButton,
{
align: 'left',
label: mw.msg( 'templatedata-modal-title-language' )
}
);
this.descriptionInput = new OO.ui.MultilineTextInputWidget( {
autosize: true
} );
this.templateDescriptionFieldset = new OO.ui.FieldsetLayout( {
items: [ this.descriptionInput ]
} );
// Add Maps panel button
this.mapsPanelButton = new OO.ui.ButtonWidget( {
label: mw.msg( 'templatedata-modal-button-map' ),
classes: [ 'mw-templateData-maps-panel-button' ]
} );
this.paramListNoticeMessage = new OO.ui.MessageWidget();
this.paramListNoticeMessage.toggle( false );
this.paramSelect = new ParamSelectWidget();
this.paramImport = new ParamImportWidget();
var templateParamsFieldset = new OO.ui.FieldsetLayout( {
label: mw.msg( 'templatedata-modal-title-templateparams' ),
items: [ this.paramSelect, this.paramImport ]
} );
this.templateFormatSelectWidget = new OO.ui.ButtonSelectWidget();
this.templateFormatSelectWidget.addItems( [
new OO.ui.ButtonOptionWidget( {
data: null,
label: mw.msg( 'templatedata-modal-format-null' )
} ),
new OO.ui.ButtonOptionWidget( {
data: 'inline',
icon: 'template-format-inline',
label: mw.msg( 'templatedata-modal-format-inline' )
} ),
new OO.ui.ButtonOptionWidget( {
data: 'block',
icon: 'template-format-block',
label: mw.msg( 'templatedata-modal-format-block' )
} ),
new OO.ui.ButtonOptionWidget( {
data: 'custom',
icon: 'settings',
label: mw.msg( 'templatedata-modal-format-custom' )
} )
] );
this.templateFormatInputWidget = new OO.ui.TextInputWidget( {
placeholder: mw.msg( 'templatedata-modal-format-placeholder' )
} );
var templateFormatFieldSet = new OO.ui.FieldsetLayout( {
label: mw.msg( 'templatedata-modal-title-templateformat' ),
items: [
new OO.ui.FieldLayout( this.templateFormatSelectWidget ),
new OO.ui.FieldLayout( this.templateFormatInputWidget, {
align: 'top',
label: mw.msg( 'templatedata-modal-title-templateformatstring' )
} )
]
} );
// Param details panel
this.$paramDetailsContainer = $( '<div>' )
.addClass( 'tdg-templateDataDialog-paramDetails' );
this.listParamsPanel.$element
.addClass( 'tdg-templateDataDialog-listParamsPanel' )
.append(
this.paramListNoticeMessage.$element,
languageActionFieldLayout.$element,
this.templateDescriptionFieldset.$element,
new OO.ui.FieldLayout( this.mapsPanelButton ).$element,
templateFormatFieldSet.$element,
templateParamsFieldset.$element
);
this.paramEditNoticeMessage = new OO.ui.MessageWidget();
this.paramEditNoticeMessage.toggle( false );
// Edit panel
this.editParamPanel.$element
.addClass( 'tdg-templateDataDialog-editParamPanel' )
.append(
this.paramEditNoticeMessage.$element,
this.$paramDetailsContainer
);
// Language panel
this.languagePanel.$element
.addClass( 'tdg-templateDataDialog-languagePanel' )
.append(
this.newLanguageSearch.$element
);
this.addParamPanel.$element
.addClass( 'tdg-templateDataDialog-addParamPanel' )
.append( addParamFieldlayout.$element );
// Maps panel
mapsListPanel.$element
.addClass( 'tdg-templateDataDialog-mapsListPanel' )
.append( this.mapsGroup.$element );
this.newMapNameInput.$element.hide();
this.cancelAddMapButton.$element.hide();
this.saveAddMapButton.$element.hide();
addNewMapButtonPanel.$element
.addClass( 'tdg-templateDataDialog-addNewMapButtonPanel' )
.append(
this.addNewMapButton.$element,
this.newMapNameInput.$element,
this.cancelAddMapButton.$element,
this.saveAddMapButton.$element
);
mapsContentPanel.$element
.addClass( 'tdg-templateDataDialog-mapsContentPanel' )
.append(
this.removeMapButton.$element,
this.templateMapsInput.$element
);
this.editMapsPanel.$element
.addClass( 'tdg-templateDataDialog-editMapsPanel' )
.append( templateMapsMenuLayout.$element );
this.panels.addItems( [
this.listParamsPanel,
this.editParamPanel,
this.languagePanel,
this.addParamPanel,
this.editMapsPanel
] );
this.panels.setItem( this.listParamsPanel );
this.panels.$element.addClass( 'tdg-templateDataDialog-panels' );
// Build param details panel
this.$paramDetailsContainer.append( this.createParamDetails() );
// Initialization
this.$body.append(
this.noticeMessage.$element,
this.panels.$element
);
// Events
this.newLanguageSearch.getResults().connect( this, { choose: 'onNewLanguageSearchResultsChoose' } );
this.newParamInput.connect( this, { change: 'onAddParamInputChange', enter: 'onAddParamButtonClick' } );
this.addParamButton.connect( this, { click: 'onAddParamButtonClick' } );
this.descriptionInput.connect( this, { change: 'onDescriptionInputChange' } );
this.languagePanelButton.connect( this, { click: 'onLanguagePanelButton' } );
this.languageDropdownWidget.getMenu().connect( this, { select: 'onLanguageDropdownWidgetSelect' } );
this.mapsPanelButton.connect( this, { click: 'onMapsPanelButton' } );
this.addNewMapButton.connect( this, { click: 'onAddNewMapClick' } );
this.cancelAddMapButton.connect( this, { click: 'onCancelAddingMap' } );
this.saveAddMapButton.connect( this, { click: 'onEmbedNewMap' } );
this.newMapNameInput.connect( this, { enter: 'onEmbedNewMap' } );
this.mapsGroup.connect( this, { select: 'onMapsGroupSelect' } );
this.removeMapButton.connect( this, { click: 'onMapItemRemove' } );
this.templateMapsInput.connect( this, { change: 'onMapInfoChange' } );
this.paramSelect.connect( this, {
choose: 'onParamSelectChoose',
reorder: 'onParamSelectReorder'
} );
this.paramImport.connect( this, { click: 'importParametersFromTemplateCode' } );
this.templateFormatSelectWidget.connect( this, { choose: 'onTemplateFormatSelectWidgetChoose' } );
this.templateFormatInputWidget.connect( this, {
change: 'onTemplateFormatInputWidgetChange',
enter: 'onTemplateFormatInputWidgetEnter'
} );
};
/**
* Respond to model change of description event
*
* @param {string} description New description
*/
Dialog.prototype.onModelChangeDescription = function ( description ) {
this.descriptionInput.setValue( description );
};
/**
* Respond to model change of map info event
*
* @param {Object|undefined} map
*/
Dialog.prototype.onModelChangeMapInfo = function ( map ) {
var selectedItem = this.mapsGroup.findSelectedItem();
map = map || {};
this.mapsCache = OO.copy( map );
if ( selectedItem ) {
this.templateMapsInput.setValue( this.stringifyObject( map[ selectedItem.label ] ) );
}
};
/**
* Respond to add param input change.
*
* @param {string} value New parameter name
*/
Dialog.prototype.onAddParamInputChange = function ( value ) {
var allProps = Model.static.getAllProperties( true );
value = value.trim();
if ( !value ||
value.match( allProps.name.restrict ) ||
(
this.model.isParamExists( value ) &&
!this.model.isParamDeleted( value )
)
) {
// Disable the add button
this.addParamButton.setDisabled( true );
} else {
this.addParamButton.setDisabled( false );
}
};
/**
* Respond to change of param order from the model
*
* @param {string[]} paramOrderArray The array of keys in order
*/
Dialog.prototype.onModelChangeParamOrder = function () {
// Refresh the parameter widget
this.repopulateParamSelectWidget();
};
/**
* Respond to change of param property from the model
*
* @param {string} paramKey Parameter key
* @param {string} prop Property name
* @param {Mixed} value
* @param {string} language
*/
Dialog.prototype.onModelChangeProperty = function ( paramKey, prop, value ) {
// Refresh the parameter widget
if ( paramKey === this.selectedParamKey && prop === 'name' ) {
this.selectedParamKey = value;
}
};
/**
* Respond to a change in the model
*/
Dialog.prototype.onModelChange = function () {
this.modified = true;
this.updateActions();
};
/**
* Set action abilities according to whether the model is modified
*/
Dialog.prototype.updateActions = function () {
this.actions.setAbilities( { apply: this.modified } );
};
/**
* Respond to param order widget reorder event
*
* @param {OO.ui.OptionWidget} item Item reordered
* @param {number} newIndex New index of the item
*/
Dialog.prototype.onParamSelectReorder = function ( item, newIndex ) {
if ( !this.paramsReordered ) {
Metrics.logEvent( 'parameter-reorder' );
}
this.paramsReordered = true;
this.model.reorderParamOrderKey( item.getData(), newIndex );
};
/**
* Respond to description input change event
*
* @param {string} value Description value
*/
Dialog.prototype.onDescriptionInputChange = function ( value ) {
if ( !this.descriptionChanged ) {
Metrics.logEvent( 'template-description-change' );
}
this.descriptionChanged = true;
if ( this.model.getTemplateDescription( this.language ) !== value ) {
this.model.setTemplateDescription( value, this.language );
}
};
/**
* Create items for the returned maps and add them to the maps group
*
* @param {Object|undefined} mapsObject
*/
Dialog.prototype.populateMapsItems = function ( mapsObject ) {
mapsObject = mapsObject || {};
var mapKeysList = Object.keys( mapsObject );
var items = mapKeysList.map( function ( mapKey ) {
return new OO.ui.OutlineOptionWidget( {
label: mapKey
} );
} );
this.mapsGroup.clearItems();
this.mapsGroup.addItems( items );
// Maps is not empty anymore
this.updateActions();
};
/**
* Respond to edit maps input change event
*
* @param {string} value map info value
*/
Dialog.prototype.onMapInfoChange = function ( value ) {
var selectedItem = this.mapsGroup.findSelectedItem();
// Update map Info
this.model.maps = this.model.getMapInfo() || {};
if ( selectedItem ) {
if ( this.model.getMapInfo()[ selectedItem.label ] !== value ) {
// Disable Done button in case of invalid JSON
try {
// This parsing method keeps only the last key/value pair if duplicate keys are defined, and does not throw an error.
// Our model will be updated with a valid maps object, but the user may lose their input if it has duplicate key.
this.mapsCache[ selectedItem.label ] = JSON.parse( value );
this.actions.setAbilities( { done: true } );
} catch ( err ) {
// Otherwise disable the done button if maps object is populated
this.actions.setAbilities( { done: false } );
} finally {
if ( this.mapsGroup.items.length === 0 ) {
this.actions.setAbilities( { done: true } );
this.removeMapButton.setDisabled( true );
}
}
}
}
};
/**
* Handle click event for Add new map button
*/
Dialog.prototype.onAddNewMapClick = function () {
// Add new text input in maps elements to prompt the map name
this.newMapNameInput.$element.show();
this.cancelAddMapButton.$element.show();
this.saveAddMapButton.$element.show();
this.addNewMapButton.$element.hide();
this.newMapNameInput.setValue( '' );
this.newMapNameInput.focus();
this.mapsGroup.selectItem( null );
// Text-area show "adding a new map.." message in templateMapsInput and disable the input.
this.templateMapsInput.setDisabled( true );
this.templateMapsInput.setValue( mw.msg( 'templatedata-modal-placeholder-add-new-map-input' ) );
// Disable the removing functionality for maps
this.removeMapButton.setDisabled( true );
// move the list panel down as add new map expanded
this.editMapsPanel.$element.addClass( 'tdg-templateDataDialog-addingNewMap' );
};
/**
* Handle clicking cancel button (for add new map panel)
*
* @param {OO.ui.OutlineOptionWidget} [highlightNext] item to be highlighted after adding a new map canceled/done
*/
Dialog.prototype.onCancelAddingMap = function ( highlightNext ) {
// Remove the text-area input, cancel button, and show add new map button
this.newMapNameInput.$element.hide();
this.cancelAddMapButton.$element.hide();
this.saveAddMapButton.$element.hide();
this.addNewMapButton.$element.show();
// move the list panel up back as add new map shrank
this.editMapsPanel.$element.removeClass( 'tdg-templateDataDialog-addingNewMap' );
this.removeMapButton.setDisabled( false );
this.mapsGroup.selectItem( highlightNext || this.mapsGroup.findFirstSelectableItem() );
};
/**
* Handle clicking Enter event for promptMapName
*
* @param {jQuery.Event} response response from Enter action on promptMapName
*/
Dialog.prototype.onEmbedNewMap = function ( response ) {
var mapNameValue = response ? response.target.value : this.newMapNameInput.getValue();
this.mapsCache = this.mapsCache || {};
// Create a new empty map in maps object
this.mapsCache[ mapNameValue ] = {};
var newlyAddedMap = new OO.ui.OutlineOptionWidget( {
label: mapNameValue
} );
// Add the new map item and select it
if ( mapNameValue.length !== 0 ) {
this.mapsGroup.addItems( [ newlyAddedMap ], 0 );
} else {
delete this.mapsCache[ mapNameValue ];
}
this.onCancelAddingMap( newlyAddedMap );
};
/**
* Handle click event for the remove button
*/
Dialog.prototype.onMapItemRemove = function () {
var item = this.mapsGroup.findSelectedItem();
if ( item ) {
this.mapsGroup.removeItems( [ item ] );
// Remove the highlighted map from maps object
delete this.mapsCache[ item.label ];
}
// Highlight another item, or show the search panel if the maps group is now empty
this.onMapsGroupSelect();
};
/**
* Respond to a map group being selected
*/
Dialog.prototype.onMapsGroupSelect = function () {
// Highlight new item
var item = this.mapsGroup.findSelectedItem();
if ( !item ) {
this.templateMapsInput.setDisabled( true );
this.templateMapsInput.setValue( '' );
} else {
// Cancel the process of adding a map, Cannot call onCancelAddingMap because these two functions
// cannot be called recursively
// Remove the text-area input, cancel button, and show add new map button
this.newMapNameInput.$element.hide();
this.cancelAddMapButton.$element.hide();
this.saveAddMapButton.$element.hide();
this.addNewMapButton.$element.show();
// move the list panel up back as add new map shrank
this.editMapsPanel.$element.removeClass( 'tdg-templateDataDialog-addingNewMap' );
this.removeMapButton.setDisabled( $.isEmptyObject( this.mapsCache ) );
this.mapsGroup.selectItem( item );
this.templateMapsInput.setDisabled( false );
// Scroll item into view in menu
OO.ui.Element.static.scrollIntoView( item.$element[ 0 ] );
// Populate the mapsContentPanel
this.mapsCache = this.mapsCache || {};
var currentMapInfo = this.mapsCache[ item.label ];
this.templateMapsInput.setValue( this.stringifyObject( currentMapInfo ) );
}
};
/**
* Stringify objects in the dialog with space of 4, mainly maps objects
*
* @param {Object} object maps object
* @return {string} serialized form
*/
Dialog.prototype.stringifyObject = function ( object ) {
return JSON.stringify( object, null, 4 );
};
/**
* Respond to add language button click
*/
Dialog.prototype.onLanguagePanelButton = function () {
this.switchPanels( this.languagePanel );
};
/**
* Respond to language select widget select event
*
* @param {OO.ui.OptionWidget} item Selected item
*/
Dialog.prototype.onLanguageDropdownWidgetSelect = function ( item ) {
var language = item ? item.getData() : this.language;
// Change current language
if ( language !== this.language ) {
this.language = language;
// Update description label
this.templateDescriptionFieldset.setLabel( mw.msg( 'templatedata-modal-title-templatedesc', this.language ) );
// Update description value
this.descriptionInput.setValue( this.model.getTemplateDescription( language ) )
.$input.attr( { lang: mw.language.bcp47( language ), dir: 'auto' } );
// Update all param descriptions in the param select widget
this.repopulateParamSelectWidget();
// Update the parameter detail page
this.updateParamDetailsLanguage();
this.emit( 'change-language', this.language );
}
};
/**
* Handle choose events from the new language search widget
*
* @param {OO.ui.OptionWidget} item Chosen item
*/
Dialog.prototype.onNewLanguageSearchResultsChoose = function ( item ) {
var newLanguage = item.getData().code;
if ( newLanguage ) {
if ( this.availableLanguages.indexOf( newLanguage ) === -1 ) {
// Add new language
this.availableLanguages.push( newLanguage );
var languageButton = new OO.ui.MenuOptionWidget( {
data: newLanguage,
label: $.uls.data.getAutonym( newLanguage )
} );
this.languageDropdownWidget.getMenu().addItems( [ languageButton ] );
}
// Select the new item
this.languageDropdownWidget.getMenu().selectItemByData( newLanguage );
}
// Go to the main panel
this.switchPanels();
};
/**
* Respond to edit maps button click
*/
Dialog.prototype.onMapsPanelButton = function () {
var item = this.mapsGroup.findSelectedItem() || this.mapsGroup.findFirstSelectableItem();
this.switchPanels( this.editMapsPanel );
// Select first item
this.mapsGroup.selectItem( item );
};
/**
* Respond to add parameter button
*/
Dialog.prototype.onAddParamButtonClick = function () {
if ( this.addParamButton.isDisabled() ) {
return;
}
var newParamKey = this.newParamInput.getValue().trim();
if ( this.model.isParamDeleted( newParamKey ) ) {
this.model.emptyParamData( newParamKey );
} else if ( !this.model.isParamExists( newParamKey ) ) {
this.model.addParam( newParamKey );
this.addParamToSelectWidget( newParamKey );
}
// Reset the input
this.newParamInput.setValue( '' );
// Go back to list
this.switchPanels();
};
/**
* Respond to choose event from the param select widget
*
* @param {OO.ui.OptionWidget} item Parameter item
*/
Dialog.prototype.onParamSelectChoose = function ( item ) {
var paramKey = item.getData();
this.selectedParamKey = paramKey;
// The panel with the `propInputs` widgets must be made visible before changing their value.
// Otherwiese the autosize feature of MultilineTextInputWidget doesn't work.
this.switchPanels( this.editParamPanel );
// Fill in parameter detail
this.getParameterDetails( paramKey );
};
/**
* Respond to choose event from the template format select widget
*
* @param {OO.ui.OptionWidget} item Format item
*/
Dialog.prototype.onTemplateFormatSelectWidgetChoose = function ( item ) {
var format = item.getData(),
shortcuts = {
inline: '{{_|_=_}}',
block: '{{_\n| _ = _\n}}'
};
if ( format !== 'custom' ) {
this.model.setTemplateFormat( format );
this.templateFormatInputWidget.setDisabled( true );
if ( format !== null ) {
this.templateFormatInputWidget.setValue(
this.formatToDisplay( shortcuts[ format ] )
);
}
} else {
this.templateFormatInputWidget.setDisabled( false );
this.onTemplateFormatInputWidgetChange(
this.templateFormatInputWidget.getValue()
);
}
};
Dialog.prototype.formatToDisplay = function ( s ) {
// Use '↵' (\u21b5) as a fancy newline (which doesn't start a new line).
return s.replace( /\n/g, '\u21b5' );
};
Dialog.prototype.displayToFormat = function ( s ) {
// Allow user to type \n or \\n (literal backslash, n) for a new line.
return s.replace( /\n|\\n|\u21b5/g, '\n' );
};
/**
* Respond to change event from the template format input widget
*
* @param {string} value Input widget value
*/
Dialog.prototype.onTemplateFormatInputWidgetChange = function ( value ) {
var item = this.templateFormatSelectWidget.findSelectedItem();
if ( item.getData() === 'custom' ) {
// Convert literal newlines or backslash-n to our fancy character
// replacement.
var format = this.displayToFormat( value );
var normalized = this.formatToDisplay( format );
if ( normalized !== value ) {
this.templateFormatInputWidget.setValue( normalized );
// Will recurse to actually set value in model.
} else {
this.model.setTemplateFormat( this.displayToFormat( value.trim() ) );
}
}
};
/**
* Respond to enter event from the template format input widget
*/
Dialog.prototype.onTemplateFormatInputWidgetEnter = function () {
/* Synthesize a '\n' when enter is pressed. */
this.templateFormatInputWidget.insertContent(
this.formatToDisplay( '\n' )
);
};
Dialog.prototype.onParamPropertyInputChange = function ( propName, value ) {
var $errors = $( [] ),
allProps = Model.static.getAllProperties( true ),
propInput = this.propInputs[ propName ],
dependentField = allProps[ propName ].textValue;
if ( allProps[ propName ].type === 'select' ) {
var selected = propInput.getMenu().findSelectedItem();
value = selected ? selected.getData() : allProps[ propName ].default;
this.toggleSuggestedValues( value );
}
if ( propName === 'name' ) {
if ( value.length === 0 ) {
$errors = $errors.add( $( '<p>' ).text( mw.msg( 'templatedata-modal-errormsg', '|', '=', '}}' ) ) );
}
if ( value !== this.selectedParamKey && this.model.getAllParamNames().indexOf( value ) !== -1 ) {
// We're changing the name. Make sure it doesn't conflict.
$errors = $errors.add( $( '<p>' ).text( mw.msg( 'templatedata-modal-errormsg-duplicate-name' ) ) );
}
}
if ( allProps[ propName ].type === 'array' ) {
value = propInput.getValue();
}
if ( allProps[ propName ].restrict ) {
if ( value.match( allProps[ propName ].restrict ) ) {
// Error! Don't fix the model
$errors = $errors.add( $( '<p>' ).text( mw.msg( 'templatedata-modal-errormsg', '|', '=', '}}' ) ) );
}
}
propInput.$element.toggleClass( 'tdg-editscreen-input-error', !!$errors.length );
// Check if there is a dependent input to activate
if ( dependentField && this.propFieldLayout[ dependentField ] ) {
// The textValue property depends on this property
// toggle its view
this.propFieldLayout[ dependentField ].toggle( !!value );
this.propInputs[ dependentField ].setValue( this.model.getParamProperty( this.selectedParamKey, dependentField ) );
}
// Validate
// FIXME: Don't read model information from the DOM
// eslint-disable-next-line no-jquery/no-global-selector
var anyInputError = !!$( '.tdg-templateDataDialog-paramInput.tdg-editscreen-input-error' ).length;
// Disable the 'done' button if there are any errors in the inputs
this.actions.setAbilities( { done: !anyInputError } );
if ( $errors.length ) {
this.toggleNoticeMessage( 'edit', true, 'error', $errors );
} else {
this.toggleNoticeMessage( 'edit', false );
this.model.setParamProperty( this.selectedParamKey, propName, value, this.language );
}
// If we're changing the aliases and the name has an error, poke its change
// handler in case that error was because of a duplicate name with its own
// aliases.
// FIXME: Don't read model information from the DOM
// eslint-disable-next-line no-jquery/no-class-state
if ( propName === 'aliases' && this.propInputs.name.$element.hasClass( 'tdg-editscreen-input-error' ) ) {
this.onParamPropertyInputChange( 'name', this.propInputs.name.getValue() );
}
this.trackPropertyChange( propName );
};
Dialog.prototype.toggleSuggestedValues = function ( type ) {
var suggestedValuesAllowedTypes = [
'content',
'line',
'number',
'string',
'unbalanced-wikitext',
'unknown'
];
// Don't show the suggested values field when the feature flag is
// disabled, or for inapplicable types.
this.propFieldLayout.suggestedvalues.toggle(
suggestedValuesAllowedTypes.indexOf( type ) !== -1
);
};
/**
* Set the parameter details in the detail panel.
*
* @param {string} paramKey
*/
Dialog.prototype.getParameterDetails = function ( paramKey ) {
var paramData = this.model.getParamData( paramKey );
var allProps = Model.static.getAllProperties( true );
this.stopParameterInputTracking();
for ( var prop in this.propInputs ) {
this.changeParamPropertyInput( paramKey, prop, paramData[ prop ], this.language );
// Show/hide dependents
if ( allProps[ prop ].textValue ) {
this.propFieldLayout[ allProps[ prop ].textValue ].toggle( !!paramData[ prop ] );
}
}
// Update suggested values field visibility
this.toggleSuggestedValues( paramData.type || allProps.type.default );
this.startParameterInputTracking( paramData );
};
Dialog.prototype.stopParameterInputTracking = function () {
this.paramPropertyChangeTracking = {};
};
/**
* Temporary metrics to understand how properties are edited, see T260343.
*
* @param {Object} paramValues parameter property values at dialog open time
*/
Dialog.prototype.startParameterInputTracking = function ( paramValues ) {
this.paramPropertyChangeTracking = {};
for ( var prop in this.propInputs ) {
// Set to true, unless one of the exceptions applies.
this.paramPropertyChangeTracking[ prop ] = !(
// Setting type when we already have a specific type.
( prop === 'type' && paramValues[ prop ] !== undefined && paramValues[ prop ] !== 'unknown' ) ||
// Setting priority but already required, suggested, or deprecated.
( ( prop === 'required' || prop === 'suggested' || prop === 'deprecated' ) &&
( paramValues.required || paramValues.suggested || paramValues.deprecated ) ) ||
// Fields ignored by tracking.
( prop === 'name' || prop === 'aliases' || prop === 'autovalue' || prop === 'deprecatedValue' )
);
}
};
Dialog.prototype.trackPropertyChange = function ( property ) {
var eventKey = ( property === 'required' || property === 'suggested' || property === 'deprecated' ) ?
'parameter-priority-change' : 'parameter-' + property + '-change';
if ( this.paramPropertyChangeTracking[ property ] ) {
Metrics.logEvent( eventKey );
}
this.paramPropertyChangeTracking[ property ] = false;
// These properties form a conceptual group; suppress additional events.
if ( property === 'required' || property === 'suggested' || property === 'deprecated' ) {
this.paramPropertyChangeTracking.required =
this.paramPropertyChangeTracking.suggested =
this.paramPropertyChangeTracking.deprecated = false;
}
};
/**
* Reset contents on reload
*/
Dialog.prototype.reset = function () {
this.language = null;
this.availableLanguages = [];
if ( this.paramSelect ) {
this.paramSelect.clearItems();
this.selectedParamKey = '';
}
if ( this.languageDropdownWidget ) {
this.languageDropdownWidget.getMenu().clearItems();
}
};
/**
* Empty and repopulate the parameter select widget.
*/
Dialog.prototype.repopulateParamSelectWidget = function () {
if ( !this.isSetup ) {
return;
}
var missingParams = this.model.getMissingParams(),
paramList = this.model.getParams(),
paramOrder = this.model.getTemplateParamOrder();
this.paramSelect.clearItems();
// Update all param descriptions in the param select widget
for ( var i in paramOrder ) {
var paramKey = paramList[ paramOrder[ i ] ];
if ( paramKey && !paramKey.deleted ) {
this.addParamToSelectWidget( paramOrder[ i ] );
}
}
// Check if there are potential parameters to add
// from the template source code
if ( missingParams.length > 0 ) {
this.paramImport
.toggle( true )
.buildParamLabel( missingParams );
} else {
this.paramImport.toggle( false );
}
};
/**
* Change parameter property
*
* @param {string} paramKey Parameter key
* @param {string} propName Property name
* @param {Mixed} [value] Property value
* @param {string} [lang] Language
*/
Dialog.prototype.changeParamPropertyInput = function ( paramKey, propName, value, lang ) {
var allProps = Model.static.getAllProperties( true ),
prop = allProps[ propName ],
propInput = this.propInputs[ propName ];
switch ( prop.type ) {
case 'select':
propInput = propInput.getMenu();
propInput.selectItem( propInput.findItemFromData( value || prop.default ) );
break;
case 'boolean':
propInput.setSelected( !!value );
break;
case 'array':
value = value || [];
propInput.setValue( value.map( function ( v ) {
// TagMultiselectWidget accepts nothing but strings or objects with a .data property
return v && v.data ? v : String( v );
} ) );
break;
default:
if ( typeof value === 'object' ) {
value = value[ lang || this.language ];
}
propInput.setValue( value || '' );
}
};
/**
* Add parameter to the list
*
* @param {string} paramKey Parameter key in the model
*/
Dialog.prototype.addParamToSelectWidget = function ( paramKey ) {
var data = this.model.getParamData( paramKey );
this.paramSelect.addItems( [ new ParamWidget( {
key: paramKey,
label: this.model.getParamValue( paramKey, 'label', this.language ),
aliases: data.aliases,
description: this.model.getParamValue( paramKey, 'description', this.language )
} )
// Forward keyboard-triggered events from the OptionWidget to the SelectWidget
.connect( this.paramSelect, { choose: [ 'emit', 'choose' ] } )
] );
};
/**
* Create the information page about individual parameters
*
* @return {jQuery} Editable details page for the parameter
*/
Dialog.prototype.createParamDetails = function () {
var paramProperties = Model.static.getAllProperties( true );
// Fieldset
var paramFieldset = new OO.ui.FieldsetLayout();
for ( var propName in paramProperties ) {
var propInput;
var config = {
multiline: paramProperties[ propName ].multiline
};
if ( paramProperties[ propName ].multiline ) {
config.autosize = true;
}
// Create the property inputs
switch ( paramProperties[ propName ].type ) {
case 'select':
propInput = new OO.ui.DropdownWidget( config );
var items = [];
for ( var i in paramProperties[ propName ].children ) {
items.push( new OO.ui.MenuOptionWidget( {
data: paramProperties[ propName ].children[ i ],
// The following messages are used here:
// * templatedata-doc-param-type-boolean, templatedata-doc-param-type-content,
// * templatedata-doc-param-type-date, templatedata-doc-param-type-line,
// * templatedata-doc-param-type-number, templatedata-doc-param-type-string,
// * templatedata-doc-param-type-unbalanced-wikitext, templatedata-doc-param-type-unknown,
// * templatedata-doc-param-type-url, templatedata-doc-param-type-wiki-file-name,
// * templatedata-doc-param-type-wiki-page-name, templatedata-doc-param-type-wiki-template-name,
// * templatedata-doc-param-type-wiki-user-name
label: mw.msg( 'templatedata-doc-param-' + propName + '-' + paramProperties[ propName ].children[ i ] )
} ) );
}
propInput.getMenu().addItems( items );
break;
case 'boolean':
propInput = new OO.ui.CheckboxInputWidget( config );
break;
case 'array':
config.allowArbitrary = true;
config.placeholder = mw.msg( 'templatedata-modal-placeholder-multiselect' );
propInput = new OO.ui.TagMultiselectWidget( config );
break;
default:
if ( config.multiline === true ) {
delete config.multiline;
propInput = new OO.ui.MultilineTextInputWidget( config );
} else {
delete config.multiline;
propInput = new AutosizeTextInputWidget( config );
}
break;
}
this.propInputs[ propName ] = propInput;
// The following classes are used here:
// * tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-aliases
// * tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-autovalue
// * tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-default
// * tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-deprecated
// * tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-deprecatedValue
// * tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-description
// * tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-example
// * tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-importoption
// * tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-importoption-subtitle
// * tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-label
// * tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-name
// * tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-required
// * tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-suggested
// * tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-suggestedvalues
// * tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-type
propInput.$element
.addClass( 'tdg-templateDataDialog-paramInput tdg-templateDataDialog-paramList-' + propName );
this.propFieldLayout[ propName ] = new OO.ui.FieldLayout( propInput, {
align: 'left',
// The following messages are used here:
// * templatedata-modal-table-param-aliases
// * templatedata-modal-table-param-autovalue
// * templatedata-modal-table-param-default
// * templatedata-modal-table-param-deprecated
// * templatedata-modal-table-param-deprecatedValue
// * templatedata-modal-table-param-description
// * templatedata-modal-table-param-example
// * templatedata-modal-table-param-importoption
// * templatedata-modal-table-param-importoption-subtitle
// * templatedata-modal-table-param-label
// * templatedata-modal-table-param-name
// * templatedata-modal-table-param-required
// * templatedata-modal-table-param-suggested
// * templatedata-modal-table-param-suggestedvalues
// * templatedata-modal-table-param-type
label: mw.msg( 'templatedata-modal-table-param-' + propName )
} );
// Event
if ( propInput instanceof OO.ui.DropdownWidget ) {
propInput.getMenu().connect( this, { choose: [ 'onParamPropertyInputChange', propName ] } );
} else {
propInput.connect( this, { change: [ 'onParamPropertyInputChange', propName ] } );
}
// Append to parameter section
paramFieldset.$element.append( this.propFieldLayout[ propName ].$element );
}
return paramFieldset.$element;
};
/**
* Update the labels for parameter property inputs that include language, so
* they show the currently used language.
*/
Dialog.prototype.updateParamDetailsLanguage = function () {
var languageProps = Model.static.getPropertiesWithLanguage();
for ( var i = 0; i < languageProps.length; i++ ) {
var prop = languageProps[ i ];
// The following messages are used here:
// * templatedata-modal-table-param-aliases
// * templatedata-modal-table-param-autovalue
// * templatedata-modal-table-param-default
// * templatedata-modal-table-param-deprecated
// * templatedata-modal-table-param-deprecatedValue
// * templatedata-modal-table-param-description
// * templatedata-modal-table-param-example
// * templatedata-modal-table-param-importoption
// * templatedata-modal-table-param-importoption-subtitle
// * templatedata-modal-table-param-label
// * templatedata-modal-table-param-name
// * templatedata-modal-table-param-required
// * templatedata-modal-table-param-suggested
// * templatedata-modal-table-param-suggestedvalues
// * templatedata-modal-table-param-type
var label = mw.msg( 'templatedata-modal-table-param-' + prop, this.language );
this.propFieldLayout[ prop ].setLabel( label );
this.propInputs[ prop ]
.$input.attr( { lang: mw.language.bcp47( this.language ), dir: 'auto' } );
}
};
/**
* Override getBodyHeight to create a tall dialog relative to the screen.
*
* @return {number} Body height
*/
Dialog.prototype.getBodyHeight = function () {
return window.innerHeight - 200;
};
/**
* Show or hide the notice message in the dialog with a set message.
*
* Hides all other notices messages when called, not just the one specified.
*
* @param {string} [type='list'] Which notice label to show: 'list', 'edit' or 'global'
* @param {boolean} [isShowing=false] Show or hide the message
* @param {string} [noticeMessageType='notice'] Message type: 'notice', 'error', 'warning', 'success'
* @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} [noticeMessageLabel] The message to display
*/
Dialog.prototype.toggleNoticeMessage = function ( type, isShowing, noticeMessageType, noticeMessageLabel ) {
// Hide all
this.noticeMessage.toggle( false );
this.paramEditNoticeMessage.toggle( false );
this.paramListNoticeMessage.toggle( false );
if ( noticeMessageLabel ) {
// See which error to display
var noticeReference;
if ( type === 'global' ) {
noticeReference = this.noticeMessage;
} else if ( type === 'edit' ) {
noticeReference = this.paramEditNoticeMessage;
} else {
noticeReference = this.paramListNoticeMessage;
}
// FIXME: Don't read model information from the DOM
// eslint-disable-next-line no-jquery/no-sizzle
isShowing = isShowing || !noticeReference.$element.is( ':visible' );
noticeReference.setLabel( noticeMessageLabel );
noticeReference.setType( noticeMessageType );
noticeReference.toggle( isShowing );
}
};
/**
* Import parameters from the source code.
*/
Dialog.prototype.importParametersFromTemplateCode = function () {
var $message = $( [] ),
state = 'success',
response = this.model.importSourceCodeParameters();
// Repopulate the list
this.repopulateParamSelectWidget();
if ( response.imported.length === 0 ) {
$message = $( '<p>' ).text( mw.msg( 'templatedata-modal-errormsg-import-noparams' ) );
state = 'error';
} else {
$message = $message.add(
$( '<p>' ).text(
mw.msg( 'templatedata-modal-notice-import-numparams', response.imported.length, response.imported.join( mw.msg( 'comma-separator' ) ) )
)
);
}
this.toggleNoticeMessage( 'list', true, state, $message );
};
/**
* Get a process for setting up a window for use.
*
* @param {Object} data Dialog opening data
* @param {Model} data.model
* @param {OO.ui.Element} data.editNoticeMessage
* @return {OO.ui.Process} Setup process
*/
Dialog.prototype.getSetupProcess = function ( data ) {
return Dialog.super.prototype.getSetupProcess.call( this, data )
.next( function () {
this.isSetup = false;
this.reset();
// The dialog must be supplied with a reference to a model
this.model = data.model;
this.modified = false;
// Hide the panels and display a spinner
this.$spinner.show();
this.panels.$element.hide();
this.toggleNoticeMessage( 'global', false );
this.toggleNoticeMessage( 'list', false );
// Start with parameter list
this.switchPanels();
// Events
this.model.connect( this, {
'change-description': 'onModelChangeDescription',
'change-map': 'onModelChangeMapInfo',
'change-paramOrder': 'onModelChangeParamOrder',
'change-property': 'onModelChangeProperty',
change: 'onModelChange'
} );
// Setup the dialog
this.setupDetailsFromModel();
this.newLanguageSearch.addResults();
var items = [],
defaultLanguage = this.model.getDefaultLanguage(),
languages = this.model.getExistingLanguageCodes();
// Bring in the editNoticeMessage from the main page
this.listParamsPanel.$element.prepend(
data.editNoticeMessage.$element
);
// Fill up the language selection
if (
languages.length === 0 ||
languages.indexOf( defaultLanguage ) === -1
) {
// Add the default language
items.push( new OO.ui.MenuOptionWidget( {
data: defaultLanguage,
label: $.uls.data.getAutonym( defaultLanguage )
} ) );
this.availableLanguages.push( defaultLanguage );
}
// Add all available languages
for ( var i = 0; i < languages.length; i++ ) {
items.push( new OO.ui.MenuOptionWidget( {
data: languages[ i ],
label: $.uls.data.getAutonym( languages[ i ] )
} ) );
// Store available languages
this.availableLanguages.push( languages[ i ] );
}
this.languageDropdownWidget.getMenu().addItems( items );
// Trigger the initial language choice
this.languageDropdownWidget.getMenu().selectItemByData( defaultLanguage );
this.isSetup = true;
this.repopulateParamSelectWidget();
// Show the panel
this.$spinner.hide();
this.panels.$element.show();
this.actions.setAbilities( { apply: false } );
}, this );
};
/**
* Set up the list of parameters from the model. This should happen
* after initialization of the model.
*/
Dialog.prototype.setupDetailsFromModel = function () {
// Set up description
this.descriptionInput.setValue( this.model.getTemplateDescription( this.language ) );
// set up maps
this.populateMapsItems( this.model.getMapInfo() );
this.mapsCache = OO.copy( this.model.getMapInfo() );
this.onMapsGroupSelect();
if ( this.model.getMapInfo() !== undefined ) {
var firstMapItem = Object.keys( this.model.getMapInfo() )[ 0 ];
this.templateMapsInput.setValue( this.stringifyObject( this.model.getMapInfo()[ firstMapItem ] ) );
} else {
this.templateMapsInput.setValue( '' );
this.templateMapsInput.setDisabled( true );
}
// Set up format
var format = this.model.getTemplateFormat();
if ( format === 'inline' || format === 'block' || format === null ) {
this.templateFormatSelectWidget.selectItemByData( format );
this.templateFormatInputWidget.setDisabled( true );
} else {
this.templateFormatSelectWidget.selectItemByData( 'custom' );
this.templateFormatInputWidget.setValue( this.formatToDisplay( format ) );
this.templateFormatInputWidget.setDisabled( false );
}
// Repopulate the parameter list
this.repopulateParamSelectWidget();
Metrics.logEvent( this.model.getOriginalTemplateDataObject() ?
'dialog-open-edit' : 'dialog-open-create' );
};
/**
* Switch between stack layout panels
*
* @param {OO.ui.PanelLayout} [panel] Panel to switch to, defaults to the first panel
*/
Dialog.prototype.switchPanels = function ( panel ) {
panel = panel || this.listParamsPanel;
this.panels.setItem( panel );
this.listParamsPanel.$element.toggle( panel === this.listParamsPanel );
this.editParamPanel.$element.toggle( panel === this.editParamPanel );
this.languagePanel.$element.toggle( panel === this.languagePanel );
this.addParamPanel.$element.toggle( panel === this.addParamPanel );
this.editMapsPanel.$element.toggle( panel === this.editMapsPanel );
switch ( panel ) {
case this.listParamsPanel:
this.actions.setMode( 'list' );
// Reset message
this.toggleNoticeMessage( 'list', false );
// Deselect parameter
this.paramSelect.selectItem( null );
// Repopulate the list to account for any changes
if ( this.model ) {
this.repopulateParamSelectWidget();
}
break;
case this.editParamPanel:
this.actions.setMode( 'edit' );
// Deselect parameter
this.paramSelect.selectItem( null );
this.editParamPanel.focus();
break;
case this.addParamPanel:
this.actions.setMode( 'add' );
this.newParamInput.focus();
break;
case this.editMapsPanel:
this.actions.setMode( 'maps' );
this.templateMapsInput.adjustSize( true ).focus();
break;
case this.languagePanel:
this.actions.setMode( 'language' );
this.newLanguageSearch.query.focus();
break;
}
};
/**
* Get a process for taking action.
*
* @param {string} [action] Symbolic name of action
* @return {OO.ui.Process} Action process
*/
Dialog.prototype.getActionProcess = function ( action ) {
if ( action === 'add' ) {
return new OO.ui.Process( function () {
this.switchPanels( this.addParamPanel );
}, this );
}
if ( action === 'done' ) {
return new OO.ui.Process( function () {
// setMapInfo with the value and keep the done button active
this.model.setMapInfo( this.mapsCache );
this.model.originalMaps = OO.copy( this.mapsCache );
this.switchPanels();
}, this );
}
if ( action === 'back' ) {
return new OO.ui.Process( function () {
this.switchPanels();
}, this );
}
if ( action === 'maps' ) {
return new OO.ui.Process( function () {
this.switchPanels( this.editMapsPanel );
}, this );
}
if ( action === 'cancel' ) {
return new OO.ui.Process( function () {
this.mapsCache = OO.copy( this.model.getOriginalMapsInfo() );
this.model.restoreOriginalMaps();
this.populateMapsItems( this.mapsCache );
this.onCancelAddingMap();
this.switchPanels();
}, this );
}
if ( action === 'delete' ) {
return new OO.ui.Process( function () {
this.model.deleteParam( this.selectedParamKey );
this.switchPanels();
}, this );
}
if ( action === 'apply' ) {
return new OO.ui.Process( function () {
Metrics.logEvent( this.model.getOriginalTemplateDataObject() ?
'save-page-edit' : 'save-page-create' );
this.emit( 'apply', this.model.outputTemplateData() );
this.close( { action: action } );
}, this );
}
if ( !action && this.modified ) {
return new OO.ui.Process( function () {
var dialog = this;
return OO.ui.confirm( mw.msg( 'templatedata-modal-confirmcancel' ) )
.then( function ( result ) {
if ( result ) {
dialog.close();
} else {
return $.Deferred().resolve().promise();
}
} );
}, this );
}
// Fallback to parent handler
return Dialog.super.prototype.getActionProcess.call( this, action );
};
module.exports = Dialog;
modules/ext.templateDataGenerator.editPage/init.js 0000666 00000000626 15133472347 0016332 0 ustar 00 /*!
* TemplateData Generator edit page init
*
* @author Moriel Schottlender
* @author Ed Sanders
*/
// We're on an edit page, but we don't know what namespace yet (e.g. when loaded by VisualEditorPluginModules)
if (
mw.config.get( 'wgCanonicalNamespace' ) === 'Template' &&
mw.config.get( 'wgPageContentModel' ) === 'wikitext'
) {
mw.loader.using( 'ext.templateDataGenerator.editTemplatePage' );
}
Back to Directory
File Manager