Jump to content

MediaWiki:Gadget-Global-WikiForm.js

From wikiNonStop

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

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
 * This gadget interacts with Module:WikiForm to produce forms that can create pages or add content to existing pages
 * Documentation: https://www.mediawiki.org/wiki/WikiForm
 * Master: https://www.mediawiki.org/wiki/MediaWiki:Gadget-Global-WikiForm.js
 * Author: User:Sophivorus
 * License: CC-BY-SA-4.0
 */
var WikiForm = {

	init: function () {
		WikiForm.buildClasses();
		$( '.WikiForm' ).each( WikiForm.makeForm );
	},

	/**
	 * Finish building our custom classes
	 */
	buildClasses: function () {
		OO.inheritClass( WikiForm.SearchInputWidget, OO.ui.TextInputWidget );
		OO.mixinClass( WikiForm.SearchInputWidget, OO.ui.mixin.LookupElement );

		OO.inheritClass( WikiForm.PropertyInputWidget, OO.ui.TextInputWidget );
		OO.mixinClass( WikiForm.PropertyInputWidget, OO.ui.mixin.LookupElement );

		OO.inheritClass( WikiForm.NominatimInputWidget, OO.ui.TextInputWidget );
		OO.mixinClass( WikiForm.NominatimInputWidget, OO.ui.mixin.LookupElement );
	},

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

		// Set the messages
		var messages = {
			'wikiform-submit': $template.data( 'submit' ) || 'Submit',
			'wikiform-submit-success': $template.data( 'submit-success' ) || 'The form was submitted, thanks!',
			'wikiform-submit-error': $template.data( 'submit-error' ) || 'Something went wrong! $1',
			'wikiform-template-error': $template.data( 'template-error' ) || 'The "template" parameter is required.',
			'wikiform-namespace-error': $template.data( 'namespace-error' ) || "Forms don't work in the Template namespace.",
			'wikiform-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( 'wikiform-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( 'wikiform-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;
			}

			// Set all the parameters that might be relevant
			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 values = $template.data( 'field' + i + '-values' );
			var search = $template.data( 'field' + i + '-values-from-search' );
			var property = $template.data( 'field' + i + '-values-from-property' );
			var service = $template.data( 'field' + i + '-values-from-service' );
			var options = $template.data( 'field' + i + '-options' );
			var min = $template.data( 'field' + i + '-min' );
			var max = $template.data( 'field' + i + '-max' );
			var selected = $template.data( 'field' + i + '-selected' ) ? true : false;

			// Make the basic field input
			var config = {
				name: name,
				value: value,
				placeholder: placeholder,
				required: required,
				disabled: disabled,
				data: { required: required }
			};
			var field = new OO.ui.TextInputWidget( config );

			// Modify it according to the type and parameters
			switch ( type ) {
				default:
					if ( values ) {
						config.options = [];
						values.split( ',' ).forEach( function ( value ) {
							value = value.trim();
							config.options.push( { data: value } );
						} );
						if ( options ) {
							options.split( ',' ).forEach( function ( option, index ) {
								option = option.trim();
								config.options[ index ].label = option;
							} );
						}
						field = new OO.ui.ComboBoxInputWidget( config );
					} else if ( search ) {
						config.data.search = search;
						field = new WikiForm.SearchInputWidget( config );
					} else if ( property ) {
						config.data.property = property;
						config.allowSuggestionsWhenEmpty = true;
						field = new WikiForm.PropertyInputWidget( config );
					} else if ( service === 'Nominatim' ) {
						field = new WikiForm.NominatimInputWidget( config );
					}
					break;

				case 'tags':
					config.allowArbitrary = true;
					if ( values ) {
						config.options = [];
						values.split( ',' ).forEach( function ( value ) {
							value = value.trim();
							config.options.push( { data: value } );
						} );
						if ( options ) {
							options.split( ',' ).forEach( function ( option, index ) {
								option = option.trim();
								config.options[ index ].label = option;
							} );
						}
					}
					field = new OO.ui.MenuTagMultiselectWidget( config );
					break;

				case 'file':
					field = new OO.ui.SelectFileInputWidget( config );
					break;

				case 'textarea':
					config.autosize = true;
					field = new OO.ui.MultilineTextInputWidget( config );
					break;

				case 'number':
					config.min = min;
					config.max = max;
					field = new OO.ui.NumberInputWidget( config );
					break;

				case 'boolean':
					value = value || 1;
					config.selected = selected;
					config.value = selected ? value : '';
					field = new OO.ui.CheckboxInputWidget( config );
					field.on( 'change', function ( field, value, selected ) {
						field.setValue( selected ? value : '' );
					}, [ field, value ] );
					break;

				case 'dropdown':
					if ( values ) {
						config.options = [];
						if ( !required ) {
							config.options.push( { label: placeholder } );
						}
						values.split( ',' ).forEach( function ( value ) {
							value = value.trim();
							config.options.push( { data: value } );
						} );
						if ( options ) {
							options.split( ',' ).forEach( function ( option, index ) {
								if ( !required ) {
									index = index + 1;
								}
								option = option;
								config.options[ index ].label = option;
							} );
						}
					}
					field = new OO.ui.DropdownInputWidget( config );
					break;

				case 'radio':
					if ( values ) {
						config.options = [];
						if ( !required ) {
							config.options.push( { label: placeholder } );
						}
						values.split( ',' ).forEach( function ( value ) {
							value = value.trim();
							config.options.push( { data: value } );
						} );
						if ( options ) {
							options.split( ',' ).forEach( function ( option, index ) {
								if ( !required ) {
									index = index + 1;
								}
								option = option;
								config.options[ index ].label = option;
							} );
						}
					}
					field = new OO.ui.RadioSelectInputWidget( config );
					break;

				case 'checkbox':
					if ( values ) {
						config.options = [];
						values.split( ',' ).forEach( function ( value ) {
							value = value.trim();
							config.options.push( { data: value } );
						} );
						if ( options ) {
							options.split( ',' ).forEach( function ( option, index ) {
								option = option;
								config.options[ index ].label = option;
							} );
						}
					}
					field = new OO.ui.CheckboxMultiselectInputWidget( config );
					break;

				case 'hidden':
					field = new OO.ui.HiddenInputWidget( config );
					break;
			}

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

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

		// Make the submit button
		var submitButton = new OO.ui.ButtonInputWidget( { label: mw.msg( 'wikiform-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, data, value;
		for ( name in fields ) {
			field = fields[ name ];
			data = field.getData();
			value = field.getValue ? field.getValue() : field.$element.val(); // Hidden inputs don't have getValue
			if ( data.required && ( !value || !value.length ) ) {
				field.focus();
				return;
			}
		}

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

		// Signal success
		submitButton.setDisabled( true );

		// Upload any files
		for ( name in fields ) {
			field = fields[ name ];
			if ( field instanceof OO.ui.SelectFileInputWidget && field.currentFiles ) {
				var file = field.currentFiles[0];
				if ( file ) {
					new mw.Api().upload( file, {
						filename: file.name,
						ignorewarnings: true // @todo Fail gracefully rather than ignore?
					} );
				}
			}
		}

		// Build the wikitext
		var template = $template.data( 'template' );
		var wikitext = '{{' + template;
		for ( name in fields ) {
			field = fields[ name ];
			value = field.getValue ? field.getValue() : field.$element.val();
			if ( Array.isArray( value ) ) {
				value = value.join( ', ' );
			}
			value = value.replace( 'C:\\fakepath\\', '' ); // Remove fakepath from any file names @todo Do better
			wikitext += '\n| ' + name + ' = ' + value;
		}
		wikitext += '\n}}';

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

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

		// Figure out where to redirect
		var redirect = $template.data( 'redirect' );
		if ( redirect ) {
			for ( name in fields ) {
				field = fields[ name ];
				value = field.getValue ? field.getValue() : field.$element.val();
				redirect = redirect.replace( '{{{' + name + '}}}', value );
			}
		}

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

	post: function ( wikitext, page, section, redirect, $template ) {
		return new mw.Api().get( {
			action: 'parse',
			page: page,
			prop: 'text',
		    formatversion: 2
		} ).always( function ( data ) {
			var prefix = $template.data( 'template-inline' ) ? '' : '\n\n';

			// 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 + '"), .mw-heading:contains("' + section + '")', html );
					if ( $header.length ) {
						sectionNumber = 1 + $header.prevAll( ':header, .mw-heading' ).length;
						wikitext = prefix + wikitext;
					}
				}
			} else if ( data !== 'missingtitle' ) {
				wikitext = prefix + 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 () {
				if ( redirect ) {
					var parts = redirect.split( '#' );
					if ( parts[0] === page && parts[1] ) {
						window.location.hash = '#' + parts[1];
						window.location.reload();
					} else {
						var url = mw.util.getUrl( redirect );
						window.location.href = url;
					}
				} else {
					$template.text( mw.msg( 'wikiform-submit-success' ) ).focus();
				}
			} ).fail( function ( code, info ) {
				$template.addClass( 'error' ).text( mw.msg( 'wikiform-submit-error', info ) ).focus();
			} );
		} );
	},

	/**
	 * Custom class for text fields with values from search suggestions
	 */
	SearchInputWidget: function ( config ) {
		OO.ui.TextInputWidget.call( this, config );
		OO.ui.mixin.LookupElement.call( this, config );
		this.getLookupRequest = function () {
			var value = this.getValue();
			var data = this.getData();
			var search = data.search.replace( '%s', value );
			var params = {
				format: 'json',
				formatversion: 2,
				action: 'query',
				list: 'search',
				srsearch: search,
			};
			return new mw.Api().get( params );
		};
		this.getLookupCacheDataFromResponse = function ( response ) {
			var values = [];
			if ( response && response.query && response.query.search ) {
				response.query.search.forEach( function ( result ) {
					var title = new mw.Title( result.title, result.ns );
					var value = title.getPrefixedText();
					values.push( value );
				} );
			}
			return values;
		};
		this.getLookupMenuOptionsFromData = WikiForm.getLookupMenuOptionsFromData;
	},

	/**
	 * Custom class for text fields with values from semantic properties
	 */
	PropertyInputWidget: function ( config ) {
		OO.ui.TextInputWidget.call( this, config );
		OO.ui.mixin.LookupElement.call( this, config );
		this.getLookupRequest = function () {
			var value = this.getValue();
			var data = this.getData();
			var property = data.property;
			var params = {
				format: 'json',
				formatversion: 2,
				action: 'smwbrowse',
				browse: 'pvalue',
				params: JSON.stringify( { property: property, search: value } ),
			};
			return new mw.Api().get( params ).fail( console.log );
		};
		this.getLookupCacheDataFromResponse = function ( response ) {
			var values = [];
			if ( response ) {
				response.query.forEach( function ( value ) {
					values.push( value );
				} );
			}
			return values;
		};
		this.getLookupMenuOptionsFromData = WikiForm.getLookupMenuOptionsFromData;
	},

	/**
	 * Custom class for text fields with values from Open Street Map's Nominatim service
	 */
	NominatimInputWidget: function ( config ) {
		OO.ui.TextInputWidget.call( this, config );
		OO.ui.mixin.LookupElement.call( this, config );
		this.getLookupRequest = function () {
			var value = this.getValue();
			var data = { q: value, format: 'json' };
			return $.get( '//nominatim.openstreetmap.org/search', data );
		};
		this.getLookupCacheDataFromResponse = function ( response ) {
			var values = [];
			response.forEach( function ( result ) {
				values.push( result.display_name );
			} );
			return values;
		};
		this.getLookupMenuOptionsFromData = WikiForm.getLookupMenuOptionsFromData;
	},

	getLookupMenuOptionsFromData: function ( values ) {
		var options = [];
		values.forEach( function ( value ) {
			var config = { data: value, label: value };
			var option = new OO.ui.MenuOptionWidget( config );
			options.push( option );
		} );
		return options;
	}
};

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