Viewing File: /home/omtekel/www/wp-content/upgrade/backup/TemplateData.tar

modules/ext.templateDataGenerator.data/SourceHandler.js000066600000020050151334723470017305 0ustar00var 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.js000066600000000233151334723470015513 0ustar00var Model = require( './Model.js' ),
	SourceHandler = require( './SourceHandler.js' );

module.exports = {
	Model: Model,
	SourceHandler: SourceHandler
};
modules/ext.templateDataGenerator.data/Model.js000066600000073544151334723470015627 0ustar00/**
 * 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.less000066600000000467151334723470026155 0ustar00.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.js000066600000006545151334723470024554 0ustar00var 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.js000066600000001470151334723470025363 0ustar00/**
 * 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.js000066600000001665151334723470024101 0ustar00/**
 * 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.js000066600000004034151334723470022732 0ustar00/**
 * 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.js000066600000002226151334723470024126 0ustar00/**
 * 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.js000066600000005213151334723470024614 0ustar00/**
 * 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.js000066600000020620151334723470020305 0ustar00var 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.less000066600000003366151334723470024311 0ustar00.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.js000066600000004073151334723470020026 0ustar00/*!
 * 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.js000066600000001250151334723470020463 0ustar00function 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.js000066600000143306151334723470020265 0ustar00var 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.js000066600000000626151334723470016332 0ustar00/*!
 * 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