MediaWiki:Gadget-Shortdesc-helper.js

From Seeds of the Word, the encyclopedia of the influence of the Gospel on culture

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.
/*  _____________________________________________________________________________
 * |                                                                             |
 * |                    === WARNING: GLOBAL GADGET FILE ===                      |
 * |                  Changes to this page affect many users.                    |
 * | Please discuss changes on the talk page or on [[WT:Gadget]] before editing. |
 * |_____________________________________________________________________________|
 *
 */
/**
 * Shortdesc helper: v3.4.12
 * Documentation at en.wikipedia.org/wiki/Wikipedia:Shortdesc_helper
 * The documentation includes instructions for using this gadget on other wikis.
 * Shows short descriptions, and allows importing wikidata descriptions, adding descriptions,
 * and easier editing of them by giving buttons and inputbox for doing so.
 * Forked from [[MediaWiki:Gadget-Page descriptions.js]] written by the TheDJ.
 * <nowiki>
*/
'use strict';
window.sdh = window.sdh || {};

/**
 * Set messages using mw.message.
 * window.sdh.messages can be used to override these messages (for e.g translations).
*/
window.sdh.initMessages = function () {
	/* These messages are used on all wikis and so need translation. */
	var messages = {
		/** Uncomment the following to change messages used in settings dialog */
		/**
		"libSettings-settings-title": "Settings",
		"libSettings-save-label": "Save settings",
		"libSettings-cancel-label": "Cancel",
		"libSettings-showDefaults-label": "Show defaults",
		"libSettings-showCurrentSettings-label": "Show current settings",
		"libSettings-save-success-message": "Settings for $1 successfully saved.",
		"libSettings-save-fail-message": "Could not save settings for $1.",
		*/
		/* Settings messages */
		'sdh-settingsDialog-title': 'Settings for Shortdesc helper',
		'sdh-header-general': 'General',
		'sdh-header-appearance': 'Appearance',
		'sdh-AddToRedirect-label': 'Allow additions of short descriptions to redirects',
		'sdh-AddToRedirect-help': 'When checked, redirects will have an "add" button to add a short description. (default off)',
		'sdh-InputWidth-label': 'Width of editing input in em (default 35)',
		'sdh-FontSize-label': 'Font size, as a percentage (default 100%)',
		/* Initial view messages */
		'sdh-missing-description': 'Missing <a href="/wiki/Wikipedia:Short description">$1 description</a>',
		/* Initial view buttons */
		'sdh-add-label': 'Add',
		'sdh-add-title': 'Add short description',
		'sdh-edit-label': 'Edit',
		'sdh-edit-title': 'Edit short description',
		/* Editing messages */
		'sdh-placeholder': 'Short description',
		'sdh-save-label': 'Save',
		'sdh-save-title': 'Save description',
		'sdh-cancel-label': 'Cancel',
		'sdh-cancel-title': 'Cancel editing',
		'sdh-settings-title': 'Settings',
		/* Wikidata summary messages */
		'sdh-wd-summary': '([[w:en:Wikipedia:Shortdesc helper|Shortdesc helper]])',
		'sdh-wd-edit-failed': 'Saving the edit to Wikidata failed.',
		'sdh-wd-edit-failed-prefix': '\n\nThe info given by Wikidata is that:\n\n'
	};

	/**
	 * These messages don't need translation as they are only used on enwiki
	 * because enwiki has the {{SHORTDESC:}} magic word.
	 */
	var enwikiMessages = {
		/* Settings messages */
		'sdh-MarkAsMinor-label': 'Mark edits as minor',
		'sdh-header-Wikidata': 'Wikidata',
		'sdh-SaveWikidata-label': 'Save changes to Wikidata',
		'sdh-SaveWikidata-help': 'Whether to update the Wikidata description when using the script.',
		'sdh-SaveWikidata-add-label': 'Only when no Wikidata description exists (default)',
		'sdh-SaveWikidata-all-label': 'On all edits',
		'sdh-SaveWikidata-never-label': 'Never',
		'sdh-ExportButton-label': 'Add a button, "export", to update the Wikidata description to match the local description.',
		/* Initial view messages */
		'sdh-wikidata-link-label': 'Wikidata',
		'sdh-no-description': 'This page has deliberately no description.',
		/* Initial view buttons */
		'sdh-infoClicky-label': '?',
		'sdh-infoClicky-title': 'Click for info',
		'sdh-override-label': 'Override',
		'sdh-override-title': 'Override current short description',
		'sdh-import-label': 'Import',
		'sdh-import-title': 'Import description from Wikidata',
		'sdh-editimport-label': 'Edit and import',
		'sdh-editimport-title': 'Edit and import description from Wikidata',
		'sdh-export-label': 'Export',
		'sdh-export-title': 'Export local short description to Wikidata',
		/* Popup text */
		'sdh-no-description-popup': 'A page is deliberately set to have an empty short description using the code {{Short description|none}}. Note, however, that for now the Wikidata short description is actually still shown if available.',
		'sdh-override-popup': '<p>While this description can be overridden with another local short description, it cannot be directly edited. This is most likely because it is automatically generated by the article\'s infobox or some other template. See <a href = "/wiki/Wikipedia:WikiProject_Short_descriptions#Auto-generated_and_bot-generated_descriptions"> this page</a> for more info.</p>',
		'sdh-disambig-popup': 'This short description should not be edited because it is automatically generated by the disambiguation template and does not need to be changed.',
		'sdh-useless-popup': 'Importing of this description has been disabled as it is too generic to be useful.',
		/* Summary messages */
		'sdh-summary-append': ' ([[Wikipedia:Shortdesc helper|Shortdesc helper]])',
		'sdh-summary-changing': 'Changing [[Wikipedia:Short description|short description]] from $1 to $2',
		'sdh-summary-adding-custom': 'Adding custom [[Wikipedia:Short description|short description]]: $2',
		'sdh-summary-importing-wikidata': 'Importing Wikidata [[Wikipedia:Short description|short description]]: $2',
		'sdh-summary-adding-local': 'Adding local [[Wikipedia:Short description|short description]]: $2, overriding Wikidata description $1',
		'sdh-summary-adding': 'Adding [[Wikipedia:Short description|short description]]: $2',
		/* Failure message */
		'sdh-edit-failed': 'Saving the addition of or edit to the short description failed.',
		'sdh-edit-failed-no-template': 'Edit failed, as no short description template was found in the page wikitext. This is probably due to an edit conflict.'
	};

	/**
	 * Setting window.sdh.messages last means it overrides previous messages
	 * Thus allowing translations to override previous messages.
	 */
	mw.messages.set( messages );
	mw.messages.set( enwikiMessages );
	mw.messages.set( window.sdh.messages );
};

window.sdh.main = function () {
	/**
	 * What section the short description is in, to be determined later
	 * by searching the DOM. Used so that if the short description is in the lead
	 * only the wikitext of section 0 needs to be downloaded.
	 * @type {number}
	 */
	var section;

	// Consts
	/**
	 * Selector to find the short description in the DOM.
	 * @type {string}
	*/
	var SDELEMENT = '.shortdescription';

	/**
	 * Selector to find disambiguation template.
	 * @type {string}
	 */
	var DISAMBIGELEMENT = '#disambigbox';

	/**
	 * Search pattern for finding short description in wikitext.
 	 * Group 1 in the regex is the short description.
	 * @type {RegExp}
	*/
	var PATTERN = /\{\{[Ss]hort description\|(.*?)\}\}/;

	/**
	 * List of Wikidata descriptions that are not useful enough to be directly imported.
	 * @type {Array}
	 */
	var USELESS_DESCRIPTIONS = [
		'Wikimedia project page'
	];

	// Config variables
	var title = mw.config.get( 'wgPageName' );
	var namespace = mw.config.get( 'wgNamespaceNumber' );
	var wgQid = mw.config.get( 'wgWikibaseItemId' );
	var language = mw.config.get( 'wgContentLanguage' );
	var canEdit = mw.config.get( 'wgIsProbablyEditable' );
	var isRedirect = mw.config.get( 'wgIsRedirect' );
	var DBName = mw.config.get( 'wgDBname' );

	/**
	 * onlyEditWikidata is a site-wide flag.
	 * If it is true, then the only descriptions for the wiki are assumed to be on Wikidata.
	 * If it is false, then that means descriptions can also be added through {{SHORTDESC:}}
	 * (currently, this is only the case on enwiki).
	 * This flag modifies the behaviour of various methods to display the appropriate buttons and
	 * settings, and make the description saved to the right place.
	 * @type {boolean}
	*/
	var onlyEditWikidata = ( DBName !== 'enwiki' );

	/**
	 * Check if the user can edit the page,
	 * and disallow editing of templates and categories to prevent accidental addition.
	 * @type {boolean}
	 */
	var allowEditing = (
		(
			canEdit &&
			[ 10, 14, 710, 828, 2300, 2302 ].indexOf( namespace ) === -1
		)
	);

	// Define user agent when accessing the API
	var APIoptions = {
		ajax: {
			headers: {
				'Api-User-Agent': 'Short description editer/viewer gadget (w:en:Wikipedia:Shortdesc helper)'
			}
		}
	};

	var API = new mw.Api( APIoptions );

	/**
	 * Get the wikitext of the page.
	 * @return {Promise}
	 */
	var getText = function () {
		return API.get( {
			action: 'query',
			prop: 'revisions',
			titles: title,
			rvprop: 'content',
			rvsection: section,
			rvslots: 'main',
			formatversion: 2
		} );
	};

	/**
	 * Download wikitext if it is a local description.
	 * Whether it is a local description is determined through searching the DOM
	 * since waiting for the description API query to complete would delay
	 * showing the short description. Also, whether to download the whole wikitext,
	 * or only the lead section wikitext is determined.
	 * @type {Promise}
	 */
	var callPromiseText = ( function () {
		var elements;
		if ( onlyEditWikidata ) {
			return;
		}
		// FIXME: should be doing this on wikipage.content hook or after $.ready
		if ( $( SDELEMENT ).length > 0 ) {
			/**
			 * Find whether the short description is in the first section, to determine
			 * if we need to download the wikitext of the entire page.
			 * Do this by searching elements above the first heading for ".shortdescription"
			 */
			// eslint-disable-next-line no-jquery/no-global-selector
			elements = $( '.mw-parser-output > h2' ).first().prevAll();
			/**
			 * Need to check sibling elements with filter and their children
			 * with find to find short description. If length > 0 then found
			 * short description before the first heading, so get wikitext of section 0.
			 */
			if ( elements.filter( SDELEMENT ).add( elements.find( SDELEMENT ) ).length > 0 ) {
				section = 0;
			}

			// Get the wikitext
			return getText();
		}
	}() );

	/**
	 * Get the short description
	 * @type {Promise}
	 */
	var callPromiseDescription = API.get( {
		action: 'query',
		titles: title,
		prop: 'description',
		formatversion: 2
	} );

	/**
	 * Load settings using libSettings if it exists
	 * Otherwise gracefully fallback to defaults.
	 */
	var usinglibSettings = !!mw.libs.libSettings;
	var ls, optionsConfig, settings, options;

	if ( usinglibSettings ) {
		ls = mw.libs.libSettings;

		optionsConfig = new ls.OptionsConfig( [
			new ls.Page( {
				title: mw.msg( 'sdh-header-general' ),
				preferences: [
					new ls.CheckboxOption( {
						name: 'MarkAsMinor',
						label: mw.msg( 'sdh-MarkAsMinor-label' ),
						defaultValue: false,
						hide: onlyEditWikidata
					} ),
					new ls.CheckboxOption( {
						name: 'AddToRedirect',
						label: mw.msg( 'sdh-AddToRedirect-label' ),
						help: mw.msg( 'sdh-AddToRedirect-help' ),
						defaultValue: false
					} ),
					new ls.CheckboxOption( {
						name: 'ExportButton',
						label: mw.msg( 'sdh-ExportButton-label' ),
						defaultValue: false,
						hide: onlyEditWikidata
					} ),
					new ls.DropdownOption( {
						name: 'SaveWikidata',
						label: mw.msg( 'sdh-SaveWikidata-label' ),
						help: mw.msg( 'sdh-SaveWikidata-help' ),
						defaultValue: 'add',
						values: [
							{ data: 'add', label: mw.msg( 'sdh-SaveWikidata-add-label' ) },
							// { data: 'all', label: mw.msg( 'sdh-SaveWikidata-all-label' ) }, // Option for all disabled due to issues with people not using it properly
							{ data: 'never', label: mw.msg( 'sdh-SaveWikidata-never-label' ) }
						],
						hide: onlyEditWikidata
					} )
				]
			} ),
			new ls.Page( {
				title: mw.msg( 'sdh-header-appearance' ),
				preferences: [
					new ls.NumberOption( {
						name: 'InputWidth',
						label: mw.msg( 'sdh-InputWidth-label' ),
						defaultValue: 35,
						UIconfig: {
							min: 10,
							max: 999
						}
					} ),
					new ls.NumberOption( {
						name: 'FontSize',
						label: mw.msg( 'sdh-FontSize-label' ),
						defaultValue: 100,
						UIconfig: {
							min: 10,
							max: 500
						}
					} )
				]
			} )
		] );

		settings = new mw.libs.libSettings.Settings( {
			title: mw.msg( 'sdh-settingsDialog-title' ),
			scriptName: 'Shortdesc-helper',
			helpInline: true,
			size: 'large',
			height: 300,
			optionsConfig: optionsConfig
		} );

		options = settings.get();
	} else {
		// Use defaults
		options = {
			MarkAsMinor: false,
			AddToRedirect: false,
			InputWidth: 35,
			FontSize: 100,
			ExportButton: false,
			SaveWikidata: 'add'
		};
	}

	// Dynamic CSS based on options
	mw.util.addCSS(
		'#sdh { font-size:' + options.FontSize + '%}' +
		'#sdh-editbox, #sdh-inputbox { max-width:' + options.InputWidth + 'em };'
	);

	/* Execute main code once the short description is gotten */
	callPromiseDescription.then( function ( response ) {
		/**
		 * These two variables are UI elements that need to be closed and reopened,
		 * and so need to be accessed outside the scope of the functions
		 * that define them.
		 */

		/**
		 * Used in InfoClickyPopup
		 * @type {OO.ui.PopupWidget}
		 */
		var infoPopup;

		/**
		 * Used in textInput
		 * @type {OO.ui.ActionFieldLayout}
		 */
		var actionField;

		/**
		 * These three variables are defined by the button being clicked
		 */

		/**
		 * The message to be used for the summary
		 * @type {string}
		 */
		var summaryMsg;

		/**
		 * Is the action a change to an existing local description
		 * or an addition, importation etc.
		 * @type {boolean}
		 */
		var change;

		/**
		 * True when there is no description anywhere, and so
		 * description should be added to Wikidata when options.SaveWikidata is 'add'.
		 * @type {boolean}
		 */
		var addWikidata;

		/**
		 * Whether there should be text initially in the input box.
		 * @type {boolean}
		 */
		var emptyPreload = false;

		/**
		 * Various HTML elements that make up
		 */
		var $sdh = $( '<div>' ).prop( 'id', 'sdh' );
		var $description = $( '<div>' ).addClass( 'sdh-showdescrip' );
		var $clickies = $( '<span>' ).addClass( 'sdh-clickies' );

		var pages = response.query.pages[ 0 ];

		/**
		 * The page short description.
		 * @type {string}
		 */
		var pageDescription = pages.description;

		/**
		 * Is the description from Wikidata (non local) or the {{SHORTDESC:}} magic word?
		 * @type {boolean}
		 */
		var isLocal = ( pages.descriptionsource === 'local' );

		/**
	 	 * Whether this is a disambiguation/set index page or not, determined by searching the DOM.
	 	 * If it is, then the option to override the short description will be disabled.
	 	 * @type {boolean}
	 	*/
		var disambigPage = $( DISAMBIGELEMENT ).length > 0;

		/**
		 * Whether a Wikidata description is too generic to be useful.
		 * @type {boolean}
		 */
		var uselessDescription = !isLocal && USELESS_DESCRIPTIONS.indexOf( pageDescription ) !== -1;

		/**
		 * Creates "clickies", simple link buttons.
		 * Things are made nice per https://stackoverflow.com/a/10510353
		 * Links are wrapped in spans to allow separators to be added using css
		 * without becoming part of the link.
		 * @param {string} msgName
		 * @param {Function} func
		 * @return {Object}
		 */
		var Clicky = function ( msgName, func ) {
			return $( '<span>' )
				.addClass( 'sdh-clicky' )
				.append( $( '<a>' )
					.attr( {
						title: mw.msg( msgName + '-title' ),
						role: 'button',
						tabindex: '0'
					} )
					.text( mw.msg( msgName + '-label' ) )
					.on( 'click', func )
					.on( 'keydown', function ( e ) {
						if ( [ 13, 32 ].indexOf( event.which ) !== -1 ) { // Space and enter
							e.preventDefault();
							return func();
						}
					} )
				);
		};

		/**
		 * Create a Clicky that opens a OOui PopupWidget.
		 * @param {string} text
		 * @return {Clicky}
		 */
		var InfoClickyPopup = function ( text ) {
			var self = this;
			self.text = text;

			self.infoClicky = new Clicky(
				'sdh-infoClicky',
				function () {
					if ( !infoPopup ) {
						mw.loader.using( [ 'oojs-ui-core', 'oojs-ui-widgets' ] ).then( function () {
							infoPopup = new OO.ui.PopupWidget( {
								$content: $( '<span>' ).append( self.text ),
								$autoCloseIgnore: self.infoClicky,
								padded: true,
								autoClose: true,
								width: 300,
								position: 'after'
							} );
							$clickies.append( infoPopup.$element );
							infoPopup.toggle();
						} );
					} else {
						infoPopup.toggle();
					}
				}
			);

			return self.infoClicky;
		};

		/**
		 * Creates OOui buttons, which are used for save and cancel.
		 * @param {string} msgName
		 * @param {Function} func
		 * @param {Array<string>} flags
		 * @param {string} icon
		 * @return {OO.ui.ButtonWidget}
		 */
		var OOuiClicky = function ( msgName, func, flags, icon ) {
			return new OO.ui.ButtonWidget( {
				label: mw.msg( msgName + '-label' ),
				icon: icon,
				title: mw.msg( msgName + '-title' ),
				flags: flags,
				classes: [ 'sdh-ooui-clicky' ]
			} ).on( 'click', func );
		};

		/**
		 * Function to check if the short description is in the wikitext.
 		 * If it is, return the wikitext and short description as defined in the text
		 * @param {Object} wikitextResult
		 * @return {Array}
		 */
		var shortdescInText = function ( wikitextResult ) {
			var wikitext = wikitextResult.query.pages[ 0 ].revisions[ 0 ].slots.main.content;
			var match = wikitext && wikitext.match( PATTERN );
			if ( match ) {
				return [ wikitext, match[ 1 ] ];
			} else {
				return [ wikitext, false ];
			}
		};

		/**
		 * Notify the user that the edit failed and log any debug info.
		 * @param {string} msgName
		 * @param {*} debug
		 * @param {string} extraMsg
		 */
		var editFailed = function ( msgName, debug, extraMsg ) {
			var message = mw.msg( msgName ) + extraMsg;
			mw.notify(
				message,
				{
					autoHide: false
				}
			);
			if ( debug ) {
				mw.log.warn( debug );
			}
		};

		/**
		 * Set the Wikidata description using the API.
		 * @param {string} newDescription
		 * @return {Promise}
		 */
		var setWikidataDescription = function ( newDescription ) {
			return mw.loader.using( 'mediawiki.ForeignApi' ).then( function () {
				var wikidataAPI = new mw.ForeignApi( 'https://www.wikidata.org/w/api.php', APIoptions );
				return wikidataAPI.postWithToken( 'csrf', {
					action: 'wbsetdescription',
					id: wgQid,
					language: language,
					summary: mw.message( 'sdh-wd-summary', language ).plain(),
					value: newDescription
				} );
			} );
		};

		/**
		 * This function edits Wikidata descriptions and is used on wikis that aren't enwiki.
		 * Beyond what setWikidataDescription does, it reloads the page on success
		 * and gives an informative error notification.
		 * @param {string} newDescription
		 */
		var editWikidataDescription = function ( newDescription ) {
			setWikidataDescription( newDescription ).then(
				function () {
					window.location.reload();
				},
				function () {
					editFailed(
						'sdh-wd-edit-failed',
						arguments,
						arguments[ 1 ].error.info ? (
							mw.msg( 'sdh-wd-edit-failed-prefix' ) +
							arguments[ 1 ].error.info
						) : ''
					);
				}
			);
		};

		/**
		 * This function adds or replaces short descriptions.
		 * @param {string} newDescription
		 */
		var editDescription = function ( newDescription ) {
			var replacement, prependText, appendText, text;

			/**
			 * Helper function to add quotes around text,
			 * used when generating the summary.
			 * @param {string} text
			 * @return {string}
			 */
			var quotify = function ( text ) {
				if ( text === '' || text === 'none' ) {
					return 'none';
				} else {
					return '"' + text + '"';
				}
			};

			/**
			 * Appends, prepends, or replaces the wikitext.
			 * depending on which of text, prependText, and appendText exists.
			 */
			var makeEdit = function () {
				var summary = mw.message(
					summaryMsg,
					quotify( pageDescription ),
					quotify( newDescription )
				).plain() +
					mw.message( 'sdh-summary-append' ).plain();
				API.postWithEditToken( {
					action: 'edit',
					section: section,
					text: text,
					title: title,
					prependtext: prependText,
					appendtext: appendText,
					summary: summary,
					minor: options.MarkAsMinor
				} ).then(
					function () {
						// Reload the page
						window.location.reload();
					},
					function () {
						editFailed( 'sdh-edit-failed', arguments );
					}
				);
			};

			/**
			 * Replaces the current local short description with the new one.
			 * If the short description doesn't exist in the text, return false.
			 * @param {string} wikitextResult Result of getText()
			 * @return {boolean} Whether there was a description in the wikitext
			 * and so whether makeEdit could be called.
			 */
			var replaceAndEdit = function ( wikitextResult ) {
				var output = shortdescInText( wikitextResult );
				var oldtext = output[ 0 ];
				var descriptionFromText = output[ 1 ];
				if ( descriptionFromText ) {
					text = oldtext.replace( PATTERN, replacement );
					makeEdit();
					return true;
				} else {
					return false;
				}
			};

			// Make edits to Wikidata as appropiate
			if (
				wgQid &&
				( options.SaveWikidata === 'add' && addWikidata ) && // options.SaveWikidata === 'all'
				newDescription !== ''
			) {
				setWikidataDescription( newDescription );
			}

			// Capitalize first letter by default unless editing local description
			if ( !isLocal ) {
				newDescription = (
					newDescription.charAt( 0 ).toUpperCase() +
					newDescription.slice( 1 )
				);
			}

			if ( newDescription === '' ) {
				newDescription = 'none';
			}

			replacement = '{{short description|' + newDescription + '}}';

			/**
			 * change = true means there was a previous short description in the wikitext
			 * that needs to be replaced.
			 */
			if ( change ) {
				/**
				 * Get the wikitext again right before making the edit
				 * to avoid issues with edit conflicts, and make the edit.
				 */
				getText().then( function ( result ) {
					if ( !replaceAndEdit( result ) ) {
						editFailed( 'sdh-edit-failed' );
					}
				} );
			} else {
				if ( isRedirect ) {
					appendText = '\n' + replacement;
				} else {
					prependText = replacement + '\n';
				}
				makeEdit();
			}
		};

		/**
		 * Creates input box with save and cancel buttons.
		 * If input box was created before, show it again.
	 	 * Otherwise, create the input box using OOui.
		*/
		var textInput = function () {
			if ( actionField ) {
				$description.addClass( 'sdh-showdescrip-hidden' );
				actionField.toggle();
			} else {
				mw.loader.using( [ 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui.styles.icons-interactions' ] ).then( function () {
					var length, saveInput, buttons;
					// Define the input box and buttons.
					var descriptionInput = new OO.ui.TextInputWidget( {
						autocomplete: false,
						autofocus: true,
						id: [ 'sdh-inputbox' ],
						label: '0',
						value: emptyPreload ? '' : pageDescription,
						placeholder: mw.msg( 'sdh-placeholder' )
					} );

					var saveButton = new OOuiClicky(
						'sdh-save',
						function () {
							saveInput();
						},
						[ 'primary', 'progressive' ]
					);

					var cancelButton = new OOuiClicky(
						'sdh-cancel',
						function () {
							actionField.toggle();
							$description.removeClass( 'sdh-showdescrip-hidden' );
						},
						[ 'safe', 'destructive' ]
					);

					var settingsButton = new OO.ui.ButtonWidget( {
						icon: 'settings',
						framed: false,
						title: mw.msg( 'sdh-settings-title' ),
						flags: [ 'safe' ],
						classes: [ 'sdh-ooui-clicky' ]
					} ).on( 'click', function () {
						settings.display();
					} );

					// On change, update character count label.
					var updateOnChange = function () {
						length = descriptionInput.getInputLength();
						descriptionInput.setLabel( String( length ) );
					};

					var items = [ saveButton, cancelButton ];

					if ( usinglibSettings ) {
						items.push( settingsButton );
					}

					buttons = new OO.ui.ButtonGroupWidget( {
						items: items
					} );

					/**
					 * This is bound to the save button.
					 * Disables all the elements and calls the relevant function
					 * responsible for saving the the entered short description.
					*/
					saveInput = function () {
						var description = descriptionInput.getValue().trim();
						descriptionInput
							.setDisabled( true )
							.pushPending( true );
						items.forEach( function ( item ) {
							item.setDisabled( true );
						} );
						if ( onlyEditWikidata ) {
							editWikidataDescription( description );
						} else {
							editDescription( description );
						}
					};

					actionField = new OO.ui.ActionFieldLayout(
						descriptionInput,
						buttons,
						{
							align: 'top',
							id: 'sdh-editbox'
						}
					);

					// Initial character count
					updateOnChange();

					descriptionInput.on( 'change', updateOnChange );
					descriptionInput.on( 'enter', saveInput );

					// Hide previous displayed clickies and add to DOM
					$description.addClass( 'sdh-showdescrip-hidden' );
					$sdh.append( actionField.$element );
				} );
			}
		};

		/**
		 * Create the html and append it to the DOM
		 * @param {Object} textElement
		 * @param {Array<Clicky>} clickyElements
		 * @param {InfoClickyPopup} popupElement
		 */
		var updateSDH = function ( textElement, clickyElements, popupElement ) {
			if ( popupElement ) {
				clickyElements.push( popupElement );
			}

			$description.append( textElement );

			if ( clickyElements.length > 0 ) {
				$clickies.append( clickyElements );
				$description.append( $clickies );
			}

			$sdh.append( $description );

			$.ready.then( function () {
				// Undo padding used to fix content jump
				mw.util.addCSS( '.skin-vector.ns-0 #contentSub::after {content: none;}' );
				// Create and attach the main div to #contentSub
				// eslint-disable-next-line no-jquery/no-global-selector
				$( '#contentSub' ).append( $sdh );
			} );
		};

		/**
		 * Disable all buttons and create processing (...) animation
		 * Used by export and import buttons.
		*/
		var setProcessing = function () {
			var x;
			var $processing = $( '<div>' )
				.addClass( 'sdh-processing' );
			// Disable all clicky buttons
			$clickies
				.children( '.sdh-clicky' )
				.addClass( 'sdh-clicky-disabled' )
				.children( 'a' )
				.off();

			// Add processing ... animation
			$description.append( $processing );

			for ( x = 0; x < 3; x++ ) {
				$processing.append(
					$( '<div>' )
						.addClass( [
							'sdh-processing-dot',
							'sdh-processing-dot-' + x
						] )
						.text( '.' )
				);
			}
		};

		/**
		 * Texts, clickies, and popups contain
		 * elements that could make up the initial display.
		*/
		var texts = {
			noDescription: $( '<span>' )
				.addClass( 'sdh-no-description' )
				.text( mw.msg( 'sdh-no-description' ) ),
			missingDescription: $( '<span>' )
				.addClass( 'sdh-missing-description' )
				.html( mw.msg( 'sdh-missing-description', ( isRedirect ? 'redirect' : 'article' ) ) ),
			pageDescription: $( '<span>' )
				.addClass( 'mw-page-description ' )
				.text( pageDescription )
		};

		var clickies = {
			add: new Clicky(
				'sdh-add',
				function () {
					summaryMsg = 'sdh-summary-adding';
					addWikidata = true; // Description should be added to wikidata in this case
					textInput();
				}
			),
			addNone: new Clicky(
				'sdh-add',
				function () {
					summaryMsg = 'sdh-summary-changing';
					change = true;
					emptyPreload = true;
					textInput();
				}
			),
			addUseless: new Clicky(
				'sdh-add',
				function () {
					summaryMsg = 'sdh-summary-adding-local';
					emptyPreload = true;
					textInput();
				}
			),
			edit: new Clicky(
				'sdh-edit',
				function () {
					summaryMsg = 'sdh-summary-changing';
					change = true;
					textInput();
				}
			),
			editimport: new Clicky(
				'sdh-editimport',
				function () {
					summaryMsg = 'sdh-summary-adding-local';
					textInput();
				}
			),
			export: new Clicky(
				'sdh-export',
				function () {
					setProcessing();
					editWikidataDescription( pageDescription );
				}
			),
			import: new Clicky(
				'sdh-import',
				function () {
					setProcessing();
					summaryMsg = 'sdh-summary-importing-wikidata';
					editDescription( pageDescription );
				}
			),
			override: new Clicky(
				'sdh-override',
				function () {
					summaryMsg = 'sdh-summary-adding-custom';
					textInput();
				}
			),
			wikidataLink: $( '<span>' )
				.addClass( [
					'sdh-clicky',
					'sdh-wikidata-description'
				] )
				.append( $( '<a>' )
					.attr( 'href', 'https://www.wikidata.org/wiki/Special:SetLabelDescriptionAliases/' + wgQid + '/' + language )
					.text( mw.msg( 'sdh-wikidata-link-label' ) )
				)
		};

		var popups = {
			disambig: new InfoClickyPopup(
				mw.message( 'sdh-disambig-popup' ).plain()
			),
			noDescription: new InfoClickyPopup(
				mw.message( 'sdh-no-description-popup' ).plain()
			),
			override: new InfoClickyPopup(
				mw.message( 'sdh-override-popup' ).plain()
			),
			useless: new InfoClickyPopup(
				mw.message( 'sdh-useless-popup' ).plain()
			)
		};

		/**
		 * Depending on various factors, such as
		 * whether the description exists,
		 * whether the description is on wikidata or not,
		 * and whether the page is in mainspace,
		 * this code determines what elements should make up the initial display.
		 * updateSDH() is then called to generate the html
		 * and add that to the DOM.
		 * @param {Object} wikitextResult
		*/
		var determineElements = function ( wikitextResult ) {
			/**
			 * The description as determined from the wikitext.
			 * @type {string}
			 */
			var descriptionFromText;

			/**
			 * The short description or a message saying no description exists etc.
			 * @type {Object}
			 */
			var textElement;

			/**
			 * What the relevant buttons ("clickies") are.
			 * @type {Array<Clicky>}
			 */
			var clickyElements = [];

			/**
			 * what clickable popup explanation is there if any
			 * @type {InfoClickyPopup}
			 */
			var popupElement;

			// Whether to show "Missing article description" if applicable
			var showMissing = (
				namespace === 0 &&
				( !isRedirect || ( isRedirect && options.AddToRedirect ) )
			);

			// If not enwiki, complete logic for non-enwiki case and exit.
			if ( onlyEditWikidata ) {
				if ( pageDescription ) {
					textElement = pageDescription;
					clickyElements.push( clickies.edit );
				} else if ( showMissing ) {
					textElement = texts.missingDescription;
					clickyElements.push( clickies.add );
				}
				updateSDH( textElement, clickyElements, popupElement );
				return;
			}

			/**
			 * Determine if the short description is in the wikitext
			 * or if it is on Wikidata/generated by an infobox.
			 */
			if ( isLocal ) {
				descriptionFromText = shortdescInText( wikitextResult )[ 1 ];
			} else {
				descriptionFromText = false;
			}

			// Show wikidata link at beginning if displaying non-local description.
			if ( pageDescription && !isLocal ) {
				clickyElements.push( clickies.wikidataLink );
			}

			if ( descriptionFromText === 'none' ) {
				// Handle {{Short description|none}}
				textElement = texts.noDescription;
				clickyElements.push( clickies.addNone );
				popupElement = popups.noDescription;
			} else {
				// Handle remaining cases
				if ( pageDescription ) {
					textElement = texts.pageDescription;
					if ( isLocal ) {
						if ( descriptionFromText ) {
							clickyElements.push( clickies.edit );
						} else {
							if ( disambigPage ) {
								popupElement = popups.disambig;
							} else {
								clickyElements.push( clickies.override );
								popupElement = popups.override;
							}
						}
					} else {
						if ( uselessDescription ) {
							popupElement = popups.useless;
							clickyElements.push(
								clickies.addUseless
							);
						} else {
							clickyElements.push(
								clickies.import,
								clickies.editimport
							);
						}
					}
				} else if ( showMissing ) {
					textElement = texts.missingDescription;
					clickyElements.push( clickies.add );
				}
			}

			// Don't show clickies for editing if not allowing editing
			if ( !allowEditing ) {
				clickyElements = [];
			}

			if ( isLocal && options.ExportButton ) {
				clickyElements.push( clickies.export );
			}

			updateSDH( textElement, clickyElements, popupElement );
		};

		if ( callPromiseText ) {
			callPromiseText.then( function ( wikitextResult ) {
				determineElements( wikitextResult );
			} );
		} else {
			determineElements();
		}
	} );
};

/* Load if viewing a page normally (not in diff view) */
if (
	mw.config.get( 'wgIsArticle' ) &&
	!mw.config.get( 'wgDiffOldId' ) &&
	mw.config.get( 'wgArticleId' ) !== 0
) {
	/**
	 * Commented out due to issues reported at
	 * [[Special:PermaLink/925885151#Doubled_short_descriptions_from_Template:Infobox_settlement]].
	 * Fire on postEdit hook to load after Visual Editor saves,
	 * as VE does not actually reload the page.
	 * Unfortunately, postEdit fires both after regular edits and VE edits,
	 * so duplicate instances will be caused after a regular edit if run always
	 * on postEdit.
	 * window.sdh.hasRun is set to true below, and will be undefined after a proper reload,
	 * but not after a dynamic VE reload.
	 * FIXME: Post edit hook fires too early, meaning if an editor adds a short description using VE
	 * it won't show the right description.

	mw.hook( 'postEdit' ).add( function () {
		if ( window.sdh.hasRun ) {
			window.sdh.main();
		}
	} );

	if ( !window.sdh.hasRun ) { // Don't run twice
		window.sdh.hasRun = true;
		window.sdh.initMessages();
		window.sdh.main();
	}
	*/
	window.sdh.initMessages();
	window.sdh.main();
}
// </nowiki>