MediaWiki:Gadget-WikiForm.js

From mediawiki.org

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/**
 * This script interacts with [[Template:Form]]
 * 
 */
var WikiForm = {

	init: function () {
		// Finish building PageInputWidget
		OO.inheritClass( WikiForm.PageInputWidget, OO.ui.TextInputWidget );
		OO.mixinClass( WikiForm.PageInputWidget, OO.ui.mixin.LookupElement );

		// Finish building LocationInputWidget
		OO.inheritClass( WikiForm.LocationInputWidget, OO.ui.TextInputWidget );
		OO.mixinClass( WikiForm.LocationInputWidget, OO.ui.mixin.LookupElement );

		$( '.template-form' ).each( WikiForm.makeForm );
	},

	makeForm: function () {
		var $template = $( this );

		// Set the messages
		var messages = {
			'form-submit': $template.data( 'submit' ) || 'Submit',
			'form-submit-success': $template.data( 'submit-success' ) || 'The form was submitted, thanks!',
			'form-submit-error': $template.data( 'submit-error' ) || 'Something went wrong! $1',
			'form-template-error': $template.data( 'template-error' ) || 'The "template" parameter is required.',
			'form-namespace-error': $template.data( 'namespace-error' ) || "Forms don't work in the Template namespace.",
			'form-group-error': $template.data( 'group-error' ) || "This form is restricted to the '$1' group.",
		};
		mw.messages.set( messages );

		// Basic validation
		var template = $template.data( 'template' );
		if ( !template ) {
			$template.addClass( 'error' ).text( mw.msg( 'form-template-error' ) );
			return;
		}

		var group = $template.data( 'group' );
		var groups = mw.config.get( 'wgUserGroups' );
		if ( group && !groups.includes( group ) ) {
			$template.addClass( 'error' ).text( mw.msg( 'form-group-error', group ) );
			return;
		}

		// Make the fields and layouts
		var fields = {};
		var layouts = [];
		for ( var i = 0; i < 100; i++ ) {
			var name = $template.data( 'field' + i );
			if ( !name ) {
				continue;
			}

			// Make the field input
			var type = $template.data( 'field' + i + '-type' );
			var value = $template.data( 'field' + i + '-value' );
			var placeholder = $template.data( 'field' + i + '-placeholder' );
			var required = $template.data( 'field' + i + '-required' ) ? true : false;
			var disabled = $template.data( 'field' + i + '-disabled' ) ? true : false;
			var selected = $template.data( 'field' + i + '-selected' ) ? true : false;
			var min = $template.data( 'field' + i + '-min' );
			var max = $template.data( 'field' + i + '-max' );
			var options = $template.data( 'field' + i + '-options' );
			var category = $template.data( 'field' + i + '-category' );
			var config = {
				name: name,
				value: value,
				placeholder: placeholder,
				required: required,
				disabled: disabled,
				data: { category: category },
			};
			var field = new OO.ui.TextInputWidget( config );
			if ( type === 'hidden' ) {
				field = new OO.ui.HiddenInputWidget( config );
			}
			if ( type === 'textarea' ) {
				config.autosize = true;
				field = new OO.ui.MultilineTextInputWidget( config );
			}
			if ( type === 'number' ) {
				config.min = min;
				config.max = max;
				field = new OO.ui.NumberInputWidget( config );
			}
			if ( type === 'boolean' ) {
				value = value || 1;
				config.selected = selected;
				config.value = selected ? value : '';
				field = new OO.ui.CheckboxInputWidget( config );
				field.on( 'change', function ( selected ) {
					field.setValue( selected ? value : '' );
				} );
			}
			if ( type === 'dropdown' ) {
				if ( options ) {
					config.options = [];
					if ( !required ) {
						config.options.push( { label: placeholder } );
					}
					options.split( ',' ).forEach( function ( value ) {
						value = value.trim();
						var option = { data: value };
						config.options.push( option );
					} );
				}
				field = new OO.ui.DropdownInputWidget( config );
			}
			if ( type === 'radio' ) {
				if ( options ) {
					config.options = [];
					if ( !required ) {
						config.options.push( { label: placeholder } );
					}
					options.split( ',' ).forEach( function ( value ) {
						value = value.trim();
						var option = { data: value };
						config.options.push( option );
					} );
				}
				field = new OO.ui.RadioSelectInputWidget( config );
			}
			if ( type === 'checkbox' ) {
				if ( options ) {
					config.options = [];
					options.split( ',' ).forEach( function ( value ) {
						value = value.trim();
						var option = { data: value };
						config.options.push( option );
					} );
				}
				field = new OO.ui.CheckboxMultiselectInputWidget( config );
			}
			if ( type === 'page' ) {
				field = new WikiForm.PageInputWidget( config );
			}
			if ( type === 'location' ) {
				field = new WikiForm.LocationInputWidget( config );
			}

			// Make the field layout
			var label = $template.data( 'field' + i + '-label' ) || name;
			var help = $template.data( 'field' + i + '-help' );
			var helpInline = $template.data( 'field' + i + '-help-inline' );
			var align = type === 'boolean' ? 'inline' : 'top';
			var layout = new OO.ui.FieldLayout( field, { label: label, align: align, help: help, helpInline: helpInline } );
			if ( type === 'hidden' ) {
				layout = field;
			}

			fields[ name ] = field;
			layouts.push( layout );
		}

		// Make the submit button
		var submitButton = new OO.ui.ButtonInputWidget( { label: mw.msg( 'form-submit' ), flags: [ 'primary', 'progressive' ] } );
		var submitButtonLayout = new OO.ui.FieldLayout( submitButton, {} );
		submitButton.on( 'click', WikiForm.submit, [ $template, submitButton, fields ] );
		layouts.push( submitButtonLayout );

		var form = new OO.ui.FormLayout( { items: layouts } );
		$template.html( form.$element );
	},

	submit: function ( $template, submitButton, fields ) {

		// Check the required fields
		var name, field, $input;
		for ( name in fields ) {
			field = fields[ name ];
			$input = field.$input;
			if ( $input.attr( 'required' ) && !$input.val() ) {
				$input.focus();
				return;
			}
		}

		// Check the namespace
		if ( mw.config.get( 'wgCanonicalNamespace' ) === 'Template' ) {
			mw.notify( mw.msg( 'form-namespace-error' ) );
			return;
		}

		// Signal success
		submitButton.setDisabled( true );

		// Build the wikitext
		var template = $template.data( 'template' );
		var wikitext = '{{' + template;
		var value;
		for ( name in fields ) {
			field = fields[ name ];
			value = field.getValue();
			if ( Array.isArray( value ) ) {
				value = value.join( ', ' );
			}
			wikitext += '\n| ' + name + ' = ' + value;
		}
		wikitext += '\n}}';

		// Figure out the page where to post
		var page = $template.data( 'page' );
		for ( name in fields ) {
			field = fields[ name ];
			value = field.getValue();
			page = page.replace( '{{{' + name + '}}}', value );
		}
		if ( !page ) {
			page = mw.config.get( 'wgPageName' );
		}

		// Figure out the section where to post
		var section = $template.data( 'section' );
		for ( name in fields ) {
			field = fields[ name ];
			value = field.getValue();
			section = section.replace( '{{{' + name + '}}}', value );
		}

		// Append the wikitext to the page
		WikiForm.post( wikitext, page, section, $template );
	},

	post: function ( wikitext, page, section, $template ) {
		return new mw.Api().get( {
			action: 'parse',
			page: page,
			prop: 'text',
		    formatversion: 2
		} ).always( function ( data ) {
			console.log( page, section, data );
			// Figure out if the section already exists and its number
			var sectionNumber;
			if ( section ) {
				sectionNumber = 'new';
				if ( data !== 'missingtitle' ) {
					var html = $.parseHTML( data.parse.text );
					var $header = $( ':header:contains(' + section + ')', html );
					if ( $header.length ) {
						sectionNumber = 1 + $header.prevAll( ':header' ).length;
						wikitext = '\n\n' + wikitext;
					}
				}
			} else if ( data !== 'missingtitle' ) {
				wikitext = '\n\n' + wikitext;
			}
			var params = {
				action: 'edit',
				title: page,
				section: sectionNumber
			};
			if ( sectionNumber === 'new' ) {
				params.sectiontitle = section;
				params.text = wikitext;
			} else {
				params.appendtext = wikitext;
			}
			return new mw.Api().postWithEditToken( params ).done( function () {
				var redirect = $template.data( 'redirect' );
				var url = mw.util.getUrl( page + '#' + section );
				if ( redirect ) {
					window.location.href = url;
				} else {
					$template.text( mw.msg( 'form-submit-success' ) ).focus();
				}
			} ).fail( function ( code, info ) {
				$template.addClass( 'error' ).text( mw.msg( 'form-submit-error', info ) ).focus();
			} );
		} );
	},

	/**
	 * Custom class for page fields
	 */
	PageInputWidget: function ( config ) {
		OO.ui.TextInputWidget.call( this, config );
		OO.ui.mixin.LookupElement.call( this, config );
		this.getLookupRequest = function () {
			var value = this.getValue();
			if ( value.length < 3 ) {
				return $.when(); // Return a resolved promise
			}
			var search = 'intitle:"' + value + '"';
			var data = this.getData();
			if ( data && data.category ) {
				search += ' incategory:"' + data.category + '"';
			}
			var namespace = 0;
			if ( data && data.namespace ) {
				namespace = data.namespace;
			}
			var params = {
				format: 'json',
				formatversion: 2,
				action: 'query',
				list: 'search',
				srsearch: search,
				srnamespace: namespace,
			};
			return new mw.Api().get( params );
		};
		this.getLookupCacheDataFromResponse = function ( response ) {
			var pages = [];
			if ( response && response.query && response.query.search ) {
				response.query.search.forEach( function ( result ) {
					var title = new mw.Title( result.title, result.ns );
					var text = title.getPrefixedText();
					pages.push( text );
				} );
			}
			return pages;
		};
		this.getLookupMenuOptionsFromData = function ( pages ) {
			var items = [];
			var data = this.getData();
			pages.forEach( function ( page ) {
				var config = { data: page, label: page };
				var item = new OO.ui.MenuOptionWidget( config );
				items.push( item );
			} );
			return items;
		};
	},

	/**
	 * Custom class for location fields
	 */
	LocationInputWidget: function ( config ) {
		OO.ui.TextInputWidget.call( this, config );
		OO.ui.mixin.LookupElement.call( this, config );
		this.getLookupRequest = function () {
			var value = this.getValue();
			if ( value.length < 3 ) {
				return $.when(); // Return a resolved promise
			}
			var data = { q: value, format: 'json' };
			return $.get( '//nominatim.openstreetmap.org/search', data );
		};
		this.getLookupCacheDataFromResponse = function ( response ) {
			var locations = [];
			response.forEach( function ( location ) {
				locations.push( location.display_name );
			} );
			return locations;
		};
		this.getLookupMenuOptionsFromData = function ( locations ) {
			var items = [];
			locations.forEach( function ( location ) {
				var config = { data: location, label: location };
				var item = new OO.ui.MenuOptionWidget( config );
				items.push( item );
			} );
			return items;
		};
	}
};

mw.loader.using( [
	'mediawiki.api',
	'mediawiki.user',
	'mediawiki.util',
	'oojs-ui-core',
	'oojs-ui-widgets'
], WikiForm.init );