MediaWiki:Gadget-WishlistIntake.js

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.
// <nowiki>
mw.loader.using( ["vue","@wikimedia/codex","mediawiki.util","mediawiki.api","user.options","mediawiki.action.view.postEdit","mediawiki.confirmCloseWindow","mediawiki.jqueryMsg"] ).then( ( require ) => {
/**
 * WishlistIntake: A gadget for the intake and editing of Community Wishlist proposals.
 * From [[Community Tech]]
 * Compiled from source at https://gitlab.wikimedia.org/repos/commtech/wishlist-intake
 * Please submit code changes as a merge request to the source repository.
 */

'use strict';



function __$styleInject(css) {
    if (!css) return;

    if (typeof window == 'undefined') return;
    var style = document.createElement('style');
    style.setAttribute('media', 'screen');

    style.innerHTML = css;
    document.head.appendChild(style);
    return css;
}

var vue = require('vue');
var codex = require('@wikimedia/codex');

/**
 * The wikitext template parameters (and optional page ID) of a wish
 */
class Wish {

	/**
	 * Normalize an array of values by trimming whitespace,
	 * removing empty values, and removing duplicates.
	 *
	 * @param {Array<string>} givenArray
	 * @return {Array<string>}
	 * @private
	 */
	static normalizeArray( givenArray ) {
		// Trim whitespace.
		return givenArray.map( ( value ) => value.trim() )
			// Remove empty values and duplicates.
			.filter( ( value, index, array ) => value !== '' && array.indexOf( value ) === index );
	}

	/**
	 * Get the storage value for a parameter from an array of values.
	 *
	 * @param {Array<string>} values
	 * @return {string}
	 */
	static getValueFromArray( values ) {
		return this.normalizeArray( values ).join( Wish.DELIMITER );
	}

	/**
	 * Get an array of values from a storage value for a parameter.
	 *
	 * @param {string} value
	 * @return {Array<string>}
	 */
	static getArrayFromValue( value ) {
		return this.normalizeArray( value.split( Wish.DELIMITER ) );
	}

	/**
	 * Props values here should be identical to storage, be it wikitext or MariaDB.
	 *
	 * @param {Object} props
	 */
	constructor( props ) {
		// Non-template (metadata) properties
		this.pageId = props.pageId || null;
		this.page = props.page || '';
		this.name = props.name || '';
		this.lang = props.lang || '';
		this.updated = props.updated || '';

		// Template parameters
		this.baselang = props.baselang || '';
		this.type = props.type || '';
		this.status = props.status || '';
		this.title = props.title || '';
		this.description = props.description || '';
		this.audience = props.audience || '';
		this.tasks = props.tasks || '';
		this.proposer = props.proposer || '';
		this.created = props.created || '';
		this.projects = props.projects || '';
		this.otherproject = props.otherproject || '';
		this.area = props.area || '';
	}
}

// Delimiter for array types
Wish.DELIMITER = ',';

Wish.TYPE_FEATURE = 'feature';
Wish.TYPE_BUG = 'bug';
Wish.TYPE_CHANGE = 'change';
Wish.TYPE_UNKNOWN = '';
Wish.STATUS_DRAFT = 'draft';
Wish.STATUS_SUBMITTED = 'submitted';
Wish.STATUS_OPEN = 'open';
Wish.STATUS_IN_PROGRESS = 'started';
Wish.STATUS_DELIVERED = 'delivered';
Wish.STATUS_BLOCKED = 'blocked';
Wish.STATUS_ARCHIVED = 'archived';

var script$7 = vue.defineComponent( {
	name: 'StatusSection',
	components: {
		CdxField: codex.CdxField,
		CdxSelect: codex.CdxSelect
	},
	props: {
		status: { type: String, default: Wish.STATUS_SUBMITTED },
		disabled: { type: Boolean, default: false }
	},
	emits: [
		'update:status'
	],
	data( props ) {
		return {
			statusValue: props.status,
			statusOptions: [
				{ label: mw.msg( 'communityrequests-status-draft' ), value: Wish.STATUS_DRAFT },
				{ label: mw.msg( 'communityrequests-status-submitted' ), value: Wish.STATUS_SUBMITTED },
				{ label: mw.msg( 'communityrequests-status-open' ), value: Wish.STATUS_OPEN },
				{ label: mw.msg( 'communityrequests-status-in-progress' ), value: Wish.STATUS_IN_PROGRESS },
				{ label: mw.msg( 'communityrequests-status-delivered' ), value: Wish.STATUS_DELIVERED },
				{ label: mw.msg( 'communityrequests-status-blocked' ), value: Wish.STATUS_BLOCKED },
				{ label: mw.msg( 'communityrequests-status-archived' ), value: Wish.STATUS_ARCHIVED }
			]
		};
	}
} );

const _hoisted_1$7 = { class: "wishlist-intake-status" };

function render$7(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_cdx_select = vue.resolveComponent("cdx-select");
  const _component_cdx_field = vue.resolveComponent("cdx-field");

  return (vue.openBlock(), vue.createElementBlock("section", _hoisted_1$7, [
    vue.createVNode(_component_cdx_field, { disabled: _ctx.disabled }, {
      label: vue.withCtx(() => [
        vue.createTextVNode(" Status ")
      ]),
      description: vue.withCtx(() => [
        vue.createTextVNode(" Only staff can change the status of a wish. ")
      ]),
      default: vue.withCtx(() => [
        vue.createVNode(_component_cdx_select, {
          selected: _ctx.statusValue,
          "onUpdate:selected": [
            _cache[0] || (_cache[0] = $event => ((_ctx.statusValue) = $event)),
            _cache[1] || (_cache[1] = $event => (_ctx.$emit( 'update:status', $event )))
          ],
          "menu-items": _ctx.statusOptions
        }, null, 8 /* PROPS */, ["selected", "menu-items"])
      ]),
      _: 1 /* STABLE */
    }, 8 /* PROPS */, ["disabled"])
  ]))
}

script$7.render = render$7;

var script$6 = vue.defineComponent( {
	name: 'TypeSection',
	components: {
		CdxField: codex.CdxField,
		CdxRadio: codex.CdxRadio
	},
	props: {
		type: { type: String, default: null },
		status: { type: String, default: 'default' },
		disabled: { type: Boolean, default: false }
	},
	emits: [
		'update:type'
	],
	setup() {
		const radios = [
			{
				label: mw.msg( 'communitywishlist-wishtype-feature-label' ),
				description: mw.msg( 'communitywishlist-wishtype-feature-description' ),
				value: Wish.TYPE_FEATURE
			},
			{
				label: mw.msg( 'communitywishlist-wishtype-bug-label' ),
				description: mw.msg( 'communitywishlist-wishtype-bug-description' ),
				value: Wish.TYPE_BUG
			},
			{
				label: mw.msg( 'communitywishlist-wishtype-change-label' ),
				description: mw.msg( 'communitywishlist-wishtype-change-description' ),
				value: Wish.TYPE_CHANGE
			},
			{
				label: mw.msg( 'communitywishlist-wishtype-unknown-label' ),
				description: mw.msg( 'communitywishlist-wishtype-unknown-description' ),
				value: Wish.TYPE_UNKNOWN
			}
		];

		return {
			radios
		};
	},
	data() {
		return {
			messages: {}
		};
	},
	watch: {
		status: {
			handler( newStatus ) {
				if ( newStatus === 'error' ) {
					this.messages = { error: mw.msg( 'communitywishlist-wishtype-error' ) };
				} else {
					this.messages = {};
				}
			}
		}
	}
} );

const _hoisted_1$6 = { class: "wishlist-intake-type" };

function render$6(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_cdx_radio = vue.resolveComponent("cdx-radio");
  const _component_cdx_field = vue.resolveComponent("cdx-field");

  return (vue.openBlock(), vue.createElementBlock("section", _hoisted_1$6, [
    vue.createVNode(_component_cdx_field, {
      "is-fieldset": true,
      status: _ctx.status,
      messages: _ctx.messages,
      disabled: _ctx.disabled
    }, {
      label: vue.withCtx(() => [
        vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-wishtype-label' ).text()), 1 /* TEXT */)
      ]),
      description: vue.withCtx(() => [
        vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-wishtype-description' ).text()), 1 /* TEXT */)
      ]),
      default: vue.withCtx(() => [
        (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(_ctx.radios, (radio) => {
          return (vue.openBlock(), vue.createBlock(_component_cdx_radio, {
            key: 'radio-' + radio.value,
            "model-value": _ctx.type,
            name: "radio-group-descriptions",
            "input-value": radio.value,
            onInput: _cache[0] || (_cache[0] = $event => (_ctx.$emit( 'update:type', $event.target.value )))
          }, {
            description: vue.withCtx(() => [
              vue.createTextVNode(vue.toDisplayString(radio.description), 1 /* TEXT */)
            ]),
            default: vue.withCtx(() => [
              vue.createTextVNode(vue.toDisplayString(radio.label) + " ", 1 /* TEXT */)
            ]),
            _: 2 /* DYNAMIC */
          }, 1032 /* PROPS, DYNAMIC_SLOTS */, ["model-value", "input-value"]))
        }), 128 /* KEYED_FRAGMENT */))
      ]),
      _: 1 /* STABLE */
    }, 8 /* PROPS */, ["status", "messages", "disabled"])
  ]))
}

script$6.render = render$6;

var script$5 = vue.defineComponent( {
	name: 'ProjectSection',
	components: {
		CdxButton: codex.CdxButton,
		CdxCard: codex.CdxCard,
		CdxCheckbox: codex.CdxCheckbox,
		CdxField: codex.CdxField,
		CdxIcon: codex.CdxIcon,
		CdxLabel: codex.CdxLabel,
		CdxTextInput: codex.CdxTextInput
	},
	props: {
		projects: { type: Array, default: () => [] },
		otherproject: { type: String, default: '' },
		disabled: { type: Boolean, default: false },
		status: { type: String, default: 'default' },
		statustype: { type: String, default: 'default' }
	},
	emits: [
		'update:projects',
		'update:otherproject'
	],
	setup() {
		// A hacky way to use codex icons in user scripts is to just copy the svg for the icon
		// See: https://doc.wikimedia.org/codex/latest/icons/all-icons.html
		// TODO: find out why we can't use the JS icons (no @wikimedia/codex-icons module on-wiki) T311099
		const cdxIconCollapse = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><!----><g><path d="m2.5 15.25 7.5-7.5 7.5 7.5 1.5-1.5-9-9-9 9z"></path></g></svg>';
		const cdxIconExpand = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><!----><g><path d="m17.5 4.75-7.5 7.5-7.5-7.5L1 6.25l9 9 9-9z"></path></g></svg>';

		return {
			cdxIconExpand,
			cdxIconCollapse
		};
	},
	data() {
		return {
			/**
			 * Whether the extended project list is shown.
			 * @type {boolean}
			 */
			expanded: this.shouldBeExpanded(),
			/**
			 * Error messages to display.
			 * @see https://doc.wikimedia.org/codex/latest/components/demos/field.html#with-validation-messages
			 * @type {Object}
			 */
			messages: {}
		};
	},
	computed: {
		/**
		 * Whether the "All projects" checkbox is ticked.
		 * @return {boolean}
		 */
		allProjects() {
			return this.projects.length === 1 && this.projects[ 0 ] === 'all';
		}
	},
	methods: {
		/**
		 * Check if a project is selected.
		 * @param {string} project
		 * @return {boolean}
		 */
		isSelectedProject( project ) {
			return this.allProjects || this.projects.includes( project );
		},
		/**
		 * Handler for (de-)selecting individual projects.
		 * @param {boolean} selected
		 * @param {string} project
		 */
		onUpdateProject( selected, project ) {
			const projectList = this.getProjectList();
			// Get the full list of projects IDs.
			let currentProjects = this.allProjects ?
				projectList.map( ( p ) => p.value ) :
				this.projects;
			// If we're adding a project, and it isn't already in the list, add it.
			if ( selected && !currentProjects.includes( project ) ) {
				currentProjects.push( project );
			} else {
				// Otherwise, remove it.
				currentProjects = currentProjects.filter( ( p ) => p !== project );
			}
			// Auto-check "All projects" if all projects are selected.
			const intersection = projectList.filter( ( p ) => currentProjects.includes( p.value ) );
			const willBeAllProjects = intersection.length === projectList.length;
			if ( willBeAllProjects ) {
				currentProjects = [ 'all' ];
			} else {
				// Remove any unknown values (T362275#9912455)
				currentProjects = currentProjects.filter( ( p ) => {
					return projectList.some( ( p2 ) => p2.value === p );
				} );
			}
			// Bubble up the selected projects to WishlistIntake.
			this.$emit( 'update:projects', currentProjects );
		},
		/**
		 * Handler for clicking the project checkbox.
		 * @param {MouseEvent} event
		 * @param {string} project
		 */
		onClickProjectCheckbox( event, project ) {
			// Prevent the card from being clicked when the checkbox is clicked.
			event.stopPropagation();
			this.onUpdateProject( !this.isSelectedProject( project ), project );
		},
		/**
		 * Handler for (de-)selecting all projects.
		 * @param {boolean} selectAll
		 */
		onUpdateAllProjects( selectAll ) {
			// Auto-expand when selecting all projects, otherwise keep the current state.
			this.expanded = selectAll ? true : this.expanded;
			this.$emit( 'update:projects', selectAll ? [ 'all' ] : [] );
		},
		/**
		 * Card data for the top projects.
		 *
		 * @see https://doc.wikimedia.org/codex/latest/components/demos/card.html
		 * @return {Array<Object>}
		 */
		getTopProjects() {
			return [
				{
					// NOTE: values are mapped to localized strings in Module:Community_Wishlist.
					value: 'wikipedia',
					url: 'www.wikipedia.org',
					label: mw.msg( 'project-localized-name-group-wikipedia' ),
					thumbnail: {
						width: 200,
						height: 150,
						url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Wikipedia-logo-v2.svg/263px-Wikipedia-logo-v2.svg.png'
					}
				},
				{
					value: 'wikidata',
					url: 'www.wikidata.org',
					label: mw.msg( 'project-localized-name-wikidatawiki' ),
					thumbnail: {
						width: 200,
						height: 150,
						url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Wikidata-logo.svg/200px-Wikidata-logo.svg.png'
					}
				},
				{
					value: 'commons',
					url: 'commons.wikimedia.org',
					label: mw.msg( 'project-localized-name-commonswiki' ),
					thumbnail: {
						width: 200,
						height: 150,
						url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Commons-logo.svg/200px-Commons-logo.svg.png'
					}
				},
				{
					value: 'wikisource',
					url: 'www.wikisource.org',
					label: mw.msg( 'project-localized-name-group-wikisource' ),
					thumbnail: {
						width: 200,
						height: 150,
						url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Wikisource-logo.svg/200px-Wikisource-logo.svg.png'
					}
				}
			];
		},
		/**
		 * Card data for the extended projects.
		 *
		 * @return {Array<Object>}
		 */
		getExtendedProjects() {
			return [
				{
					value: 'wiktionary',
					url: 'www.wiktionary.org',
					label: mw.msg( 'project-localized-name-group-wiktionary' ),
					thumbnail: {
						width: 200,
						height: 150,
						url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Wiktionary-logo.svg/200px-Wiktionary-logo.svg.png'
					}
				},
				{
					value: 'wikivoyage',
					url: 'www.wikivoyage.org',
					label: mw.msg( 'project-localized-name-group-wikivoyage' ),
					thumbnail: {
						width: 200,
						height: 150,
						url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Wikivoyage-Logo-v3-icon.svg/200px-Wikivoyage-Logo-v3-icon.svg.png'
					}
				},
				{
					value: 'wikiquote',
					url: 'www.wikiquote.org',
					label: mw.msg( 'project-localized-name-group-wikiquote' ),
					thumbnail: {
						width: 200,
						height: 150,
						url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Wikiquote-logo.svg/200px-Wikiquote-logo.svg.png'
					}
				},
				{
					value: 'wikiversity',
					url: 'www.wikiversity.org',
					label: mw.msg( 'project-localized-name-group-wikiversity' ),
					thumbnail: {
						width: 200,
						height: 150,
						url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Wikiversity_logo_2017.svg/200px-Wikiversity_logo_2017.svg.png'
					}
				},
				{
					value: 'wikifunctions',
					url: 'www.wikifunctions.org',
					label: mw.msg( 'project-localized-name-wikifunctionswiki' ),
					thumbnail: {
						width: 200,
						height: 150,
						url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/Wikifunctions-logo.svg/200px-Wikifunctions-logo.svg.png'
					}
				},
				{
					value: 'wikispecies',
					url: 'www.wikispecies.org',
					label: mw.msg( 'project-localized-name-specieswiki' ),
					thumbnail: {
						width: 200,
						height: 150,
						url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikispecies-logo.svg/200px-Wikispecies-logo.svg.png'
					}
				},
				{
					value: 'wikinews',
					url: 'www.wikinews.org',
					label: mw.msg( 'project-localized-name-group-wikinews' ),
					thumbnail: {
						width: 200,
						height: 150,
						url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/24/Wikinews-logo.svg/200px-Wikinews-logo.svg.png'
					}
				},
				{
					value: 'metawiki',
					url: 'meta.wikimedia.org',
					label: mw.msg( 'project-localized-name-metawiki' ),
					thumbnail: {
						width: 200,
						height: 150,
						url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/7/75/Wikimedia_Community_Logo.svg/200px-Wikimedia_Community_Logo.svg.png'
					}
				},
				{
					value: 'wmcs',
					url: 'wikitech.wikimedia.org',
					label: mw.msg( 'wikimedia-otherprojects-cloudservices' ),
					thumbnail: {
						width: 200,
						height: 150,
						url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/Wikimedia_Cloud_Services_logo.svg/200px-Wikimedia_Cloud_Services_logo.svg.png'
					}
				}
			];
		},
		/**
		 * Get a list of projects to display.
		 * @param {boolean} [expanded=true] Whether to show all projects.
		 * @return {Array<Object>}
		 */
		getProjectList( expanded = true ) {
			if ( expanded ) {
				return this.getTopProjects().concat( this.getExtendedProjects() );
			}

			return this.getTopProjects();
		},
		/**
		 * Whether the projects list should be expanded.
		 * Note this intentionally is false when this.projects is `['all']`,
		 * unless 'otherproject' is not empty. The idea being we only auto-expand
		 * the projects list on initial load if there are projects selected that
		 * are not in the top projects list.
		 *
		 * @return {boolean}
		 */
		shouldBeExpanded() {
			return this.projects.some( ( project ) => {
				return this.getExtendedProjects()
					.some( ( p ) => p.value === project );
			} ) || this.otherproject.trim() !== '';
		}
	},
	watch: {
		statustype: {
			handler( newStatus ) {
				if ( newStatus === 'noSelection' ) {
					const otherLabel = mw.msg( 'communitywishlist-project-other-label' );
					this.messages = {
						error: mw.msg( 'communitywishlist-project-no-selection', 1, otherLabel )
					};
				} else if ( newStatus === 'invalidOther' ) {
					this.messages = {
						error: mw.msg( 'communitywishlist-project-other-error', 3 )
					};
				} else {
					this.messages = {};
				}
			}
		}
	}
} );

const _hoisted_1$5 = { class: "wishlist-intake-project" };
const _hoisted_2$2 = {
  role: "group",
  class: "cdx-docs-card-group-with-thumbnails"
};
const _hoisted_3$2 = ["aria-label"];
const _hoisted_4$1 = {
  key: 0,
  class: "wishlist-intake-project-other"
};

function render$5(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_cdx_label = vue.resolveComponent("cdx-label");
  const _component_cdx_checkbox = vue.resolveComponent("cdx-checkbox");
  const _component_cdx_card = vue.resolveComponent("cdx-card");
  const _component_cdx_text_input = vue.resolveComponent("cdx-text-input");
  const _component_cdx_field = vue.resolveComponent("cdx-field");
  const _component_cdx_icon = vue.resolveComponent("cdx-icon");
  const _component_cdx_button = vue.resolveComponent("cdx-button");

  return (vue.openBlock(), vue.createElementBlock("section", _hoisted_1$5, [
    vue.createVNode(_component_cdx_field, {
      disabled: _ctx.disabled,
      status: _ctx.status,
      messages: _ctx.messages
    }, {
      default: vue.withCtx(() => [
        vue.createVNode(_component_cdx_label, null, {
          description: vue.withCtx(() => [
            vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-project-help' ).text()), 1 /* TEXT */)
          ]),
          default: vue.withCtx(() => [
            vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-project-intro' ).text()) + " ", 1 /* TEXT */)
          ]),
          _: 1 /* STABLE */
        }),
        vue.createVNode(_component_cdx_checkbox, {
          indeterminate: _ctx.projects.length > 0 && !_ctx.allProjects,
          "model-value": _ctx.allProjects,
          "onUpdate:modelValue": _ctx.onUpdateAllProjects
        }, {
          default: vue.withCtx(() => [
            vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-project-all-projects' ).text()), 1 /* TEXT */)
          ]),
          _: 1 /* STABLE */
        }, 8 /* PROPS */, ["indeterminate", "model-value", "onUpdate:modelValue"]),
        vue.createElementVNode("div", _hoisted_2$2, [
          (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(_ctx.getProjectList( _ctx.expanded ), (project) => {
            return (vue.openBlock(), vue.createBlock(_component_cdx_card, {
              key: project.value,
              class: "cdx-docs-card-group-with-thumbnails__card",
              thumbnail: project.thumbnail,
              onClick: $event => (_ctx.onUpdateProject( !_ctx.isSelectedProject( project.value ), project.value ))
            }, {
              title: vue.withCtx(() => [
                vue.createElementVNode("span", {
                  "aria-label": `project-${ project.value }`
                }, vue.toDisplayString(project.label), 9 /* TEXT, PROPS */, _hoisted_3$2),
                (vue.openBlock(), vue.createBlock(_component_cdx_checkbox, {
                  key: 'project-' + project.value,
                  "model-value": _ctx.isSelectedProject( project.value ),
                  "input-value": project.value,
                  "aria-labelledby": 'project-' + project.value,
                  onClick: $event => (_ctx.onClickProjectCheckbox( $event, project.value ))
                }, null, 8 /* PROPS */, ["model-value", "input-value", "aria-labelledby", "onClick"]))
              ]),
              description: vue.withCtx(() => [
                vue.createTextVNode(vue.toDisplayString(project.url), 1 /* TEXT */)
              ]),
              _: 2 /* DYNAMIC */
            }, 1032 /* PROPS, DYNAMIC_SLOTS */, ["thumbnail", "onClick"]))
          }), 128 /* KEYED_FRAGMENT */))
        ]),
        (_ctx.expanded)
          ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_4$1, [
              vue.createVNode(_component_cdx_field, null, {
                label: vue.withCtx(() => [
                  vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-project-other-label' ).text()), 1 /* TEXT */)
                ]),
                description: vue.withCtx(() => [
                  vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-project-other-description' ).text()), 1 /* TEXT */)
                ]),
                default: vue.withCtx(() => [
                  vue.createVNode(_component_cdx_text_input, {
                    "model-value": _ctx.otherproject,
                    "aria-label": _ctx.$i18n( 'communitywishlist-project-other-label' ).text(),
                    onInput: _cache[0] || (_cache[0] = $event => (_ctx.$emit( 'update:otherproject', $event.target.value )))
                  }, null, 8 /* PROPS */, ["model-value", "aria-label"])
                ]),
                _: 1 /* STABLE */
              })
            ]))
          : vue.createCommentVNode("v-if", true),
        vue.createVNode(_component_cdx_button, {
          weight: "quiet",
          class: "wishlist-intake-project-toggle",
          action: "progressive",
          type: "button",
          onClick: _cache[1] || (_cache[1] = $event => (_ctx.expanded = !_ctx.expanded))
        }, {
          default: vue.withCtx(() => [
            vue.createVNode(_component_cdx_icon, {
              icon: _ctx.expanded ? _ctx.cdxIconCollapse : _ctx.cdxIconExpand
            }, null, 8 /* PROPS */, ["icon"]),
            vue.createTextVNode(" " + vue.toDisplayString(_ctx.expanded ?
					_ctx.$i18n( 'communitywishlist-project-show-less' ).text() :
					_ctx.$i18n( 'communitywishlist-project-show-all' ).text()), 1 /* TEXT */)
          ]),
          _: 1 /* STABLE */
        })
      ]),
      _: 1 /* STABLE */
    }, 8 /* PROPS */, ["disabled", "status", "messages"])
  ]))
}

__$styleInject(".wishlist-intake-project .wishlist-intake-project-other .cdx-field {\n  margin-top: 12px;\n}\n.wishlist-intake-project .cdx-card .cdx-checkbox {\n  position: absolute;\n  top: 12px;\n  right: 12px;\n}\n[dir='rtl'] .wishlist-intake-project .cdx-card .cdx-checkbox {\n  left: 12px;\n  right: auto;\n}\n.wishlist-intake-project-toggle {\n  margin-top: 16px;\n}\n.wishlist-intake-project-other {\n  margin-top: 16px;\n}\n.cdx-docs-card-group-with-thumbnails {\n  display: grid;\n  grid-template-columns: auto auto;\n  gap: 16px;\n}\n.cdx-docs-card-group-with-thumbnails p {\n  margin-top: 100px;\n  font-weight: 700;\n}\n.cdx-docs-card-group-with-thumbnails__card {\n  cursor: pointer;\n  padding: 16px;\n}\n.cdx-docs-card-group-with-thumbnails__card .cdx-card__thumbnail.cdx-thumbnail .cdx-thumbnail__image {\n  background-size: contain;\n  border: 0;\n}\n@media (max-width: calc(640px - 1px)) {\n  .cdx-docs-card-group-with-thumbnails {\n    grid-template-columns: none;\n  }\n}\n");

script$5.render = render$5;

var wishHomePage = "Community Wishlist";
var wishIntakePage = "Community Wishlist/Intake";
var wishEditParam = "editwish";
var voteRecordedStorageName = "wishlist-intake-vote-added";
var wishIndexTemplate = "Community Wishlist/Wishes";
var wishIndexTemplateAll = "Community Wishlist/Wishes/All";
var wishIndexTemplateRecent = "Community Wishlist/Wishes/Recent";
var wishIndexTemplateArchive = "Community Wishlist/Wishes/Archive";
var wishCategory = "Community Wishlist/Wishes";
var wishPagePrefix = "Community Wishlist/Wishes/";
var wishTemplate = "Community Wishlist/Wish";
var focusAreaPagePrefix = "Community Wishlist/Focus areas/";
var focusAreaVoteCountSuffix = "/Vote count";
var focusAreaTemplate = "Community Wishlist/Focus area";
var focusAreasTemplate = "Community Wishlist/Focus areas";
var focusAreaTemplateAll = "Community Wishlist/Focus areas/All";
var focusAreaTemplateTop = "Community Wishlist/Focus areas/Top";
var supportTemplate = "Community Wishlist/Support";
var messagesPage = "MediaWiki:Gadget-WishlistIntake/messages";
var interfaceMessageGroupId = "agg-Community_Wishlist_interface";
var wishesMessageGroupId = "agg-Community_Wishlist_wishes";
var maxRecentWishes = 5;
var gadgets = {
	WishlistIntake: {
		description: "A gadget for the intake and editing of Community Wishlist proposals.",
		ResourceLoader: true,
		"default": true,
		hidden: true,
		"package": true,
		files: [
			"WishlistIntake.js"
		],
		rights: [
			"editmyusercss"
		],
		categories: [
			"Community Wishlist/Intake"
		],
		dependencies: [
			"vue",
			"@wikimedia/codex",
			"mediawiki.util",
			"mediawiki.api",
			"user.options",
			"mediawiki.action.view.postEdit",
			"mediawiki.confirmCloseWindow",
			"mediawiki.jqueryMsg"
		],
		peers: [
			"WishlistIntake-pagestyles"
		]
	},
	"WishlistIntake-pagestyles": {
		peer: true,
		hidden: true,
		files: [
			"WishlistIntake-pagestyles.css"
		],
		filesOnWiki: true
	},
	WishlistManager: {
		description: "A gadget helping with management tasks for the Community Wishlist.",
		ResourceLoader: true,
		"default": true,
		hidden: true,
		"package": true,
		files: [
			"WishlistManager.js"
		],
		rights: [
			"editmyusercss"
		],
		dependencies: [
			"mediawiki.api",
			"mediawiki.util"
		],
		messages: [
			"communityrequests-edit-with-form"
		]
	},
	WishlistTranslation: {
		description: "A gadget for machine translation of pages within the Community Wishlist.",
		ResourceLoader: true,
		"package": true,
		files: [
			"WishlistTranslation.js"
		],
		categories: [
			"Community Wishlist/Intake"
		],
		dependencies: [
			"vue",
			"@wikimedia/codex",
			"mediawiki.language.names",
			"mediawiki.storage"
		],
		messages: [
			"communityrequests-translation-translatable",
			"communityrequests-translation-switch",
			"communityrequests-translation-progress",
			"communityrequests-translation-errors"
		]
	}
};
var messages = {
	"communitywishlist-form-subtitle": "Welcome to the new Community Wishlist. Please fill in the form below to submit your wish.",
	"communitywishlist-description": "Describe your problem",
	"communitywishlist-description-description": "Explain in detail the wish or problem you are addressing.",
	"communitywishlist-title": "Wish title",
	"communitywishlist-title-error": "Please enter a value for this field (between $1 and $2 {{PLURAL:$2|character|characters}}).",
	"communitywishlist-title-description": "Make sure your title contains a brief description of the wish or problem.",
	"communitywishlist-description-error": "Please enter a value for this field ($1 or more {{PLURAL:$1|character|characters}}).",
	"communitywishlist-wishtype-label": "Which type best describes your wish?",
	"communitywishlist-wishtype-description": "For submitting a policy change request, please consult the applicable project.",
	"communitywishlist-wishtype-feature-label": "Feature request",
	"communitywishlist-wishtype-feature-description": "You want new features and functions that do not exist yet.",
	"communitywishlist-wishtype-bug-label": "Bug report",
	"communitywishlist-wishtype-bug-description": "You want a problem or error fixed with existing features.",
	"communitywishlist-wishtype-change-label": "System change",
	"communitywishlist-wishtype-change-description": "You want a currently working feature or function to be changed.",
	"communitywishlist-wishtype-unknown-label": "I'm not sure or I don't know",
	"communitywishlist-wishtype-unknown-description": "After receiving your wish, we will assign a relevant type.",
	"communitywishlist-wishtype-error": "Please select a wish type.",
	"communitywishlist-project-intro": "Which projects is your wish related to?",
	"communitywishlist-project-help": "Select all projects your wish will have an impact on.",
	"communitywishlist-project-all-projects": "All projects",
	"communitywishlist-project-show-less": "Show less",
	"communitywishlist-project-show-all": "Show all",
	"communitywishlist-project-other-label": "It's something else",
	"communitywishlist-project-other-description": "e.g. gadgets, bots and external tools",
	"communitywishlist-project-other-error": "Please enter a value for this field (greater than $1 {{PLURAL:$1|character|characters}}), or select a project checkbox.",
	"communitywishlist-project-no-selection": "Please select at least $1 {{PLURAL:$1|project checkbox|project checkboxes}}, or enter a value for the \"$2\" field.",
	"communitywishlist-audience-label": "Primary affected users",
	"communitywishlist-audience-description": "Describe which user group and situation this will affect the most",
	"communitywishlist-audience-error": "Please enter a value for this field (between $1 and $2 {{PLURAL:$2|characters}}).",
	"communitywishlist-phabricator-label": "Phabricator tasks (optional)",
	"communitywishlist-phabricator-desc": "Enter Phabricator task IDs or URLs.",
	"communitywishlist-phabricator-chip-desc": "A list of Phabricator task IDs.",
	"communitywishlist-create-success": "Your wish has been submitted.",
	"communitywishlist-edit-success": "Your wish has been saved.",
	"communitywishlist-view-all-wishes": "View all wishes.",
	"communitywishlist-close": "Close",
	"communitywishlist-publish": "Publish wish",
	"communitywishlist-save": "Save changes",
	"communitywishlist-support-focus-area": "Support focus area",
	"communitywishlist-support-focus-area-dialog-title": "Support \"$1\"",
	"communitywishlist-optional-comment": "Optional comment",
	"communitywishlist-supported": "Already supported",
	"communitywishlist-support-focus-area-confirmed": "You have voted in support of this focus area.",
	"communitywishlist-unsupport-focus-area": "Remove your support vote"
};
var importedMessages = [
	"communityrequests-form-error",
	"communityrequests-status-draft",
	"communityrequests-status-submitted",
	"communityrequests-status-open",
	"communityrequests-status-in-progress",
	"communityrequests-status-delivered",
	"communityrequests-status-blocked",
	"communityrequests-status-archived",
	"communityrequests-wish-loading-error",
	"project-localized-name-commonswiki",
	"project-localized-name-group-wikinews",
	"project-localized-name-group-wikipedia",
	"project-localized-name-group-wikiquote",
	"project-localized-name-group-wikisource",
	"project-localized-name-group-wikiversity",
	"project-localized-name-group-wikivoyage",
	"project-localized-name-group-wiktionary",
	"project-localized-name-mediawikiwiki",
	"project-localized-name-metawiki",
	"project-localized-name-specieswiki",
	"project-localized-name-wikidatawiki",
	"project-localized-name-wikifunctionswiki",
	"wikimedia-otherprojects-cloudservices",
	"cancel",
	"wikimedia-copyrightwarning"
];
var config = {
	wishHomePage: wishHomePage,
	wishIntakePage: wishIntakePage,
	wishEditParam: wishEditParam,
	voteRecordedStorageName: voteRecordedStorageName,
	wishIndexTemplate: wishIndexTemplate,
	wishIndexTemplateAll: wishIndexTemplateAll,
	wishIndexTemplateRecent: wishIndexTemplateRecent,
	wishIndexTemplateArchive: wishIndexTemplateArchive,
	wishCategory: wishCategory,
	wishPagePrefix: wishPagePrefix,
	wishTemplate: wishTemplate,
	focusAreaPagePrefix: focusAreaPagePrefix,
	focusAreaVoteCountSuffix: focusAreaVoteCountSuffix,
	focusAreaTemplate: focusAreaTemplate,
	focusAreasTemplate: focusAreasTemplate,
	focusAreaTemplateAll: focusAreaTemplateAll,
	focusAreaTemplateTop: focusAreaTemplateTop,
	supportTemplate: supportTemplate,
	messagesPage: messagesPage,
	interfaceMessageGroupId: interfaceMessageGroupId,
	wishesMessageGroupId: wishesMessageGroupId,
	maxRecentWishes: maxRecentWishes,
	gadgets: gadgets,
	messages: messages,
	importedMessages: importedMessages
};

/**
 * Utility functions for the gadget
 */
class WebUtil {

	/**
	 * Get the full page name with underscores replaced by spaces.
	 * We use this instead of wgTitle because it's possible to set up
	 * the wishlist gadget for use outside the mainspace.
	 *
	 * @return {string}
	 */
	static getPageName() {
		return mw.config.get( 'wgPageName' ).replaceAll( '_', ' ' );
	}

	/**
	 * Is the current page a wish page?
	 *
	 * @return {boolean}
	 */
	static isWishPage() {
		return this.getPageName().startsWith( config.wishPagePrefix );
	}

	/**
	 * Are we currently creating a new wish?
	 *
	 * @return {boolean}
	 */
	static isNewWish() {
		return this.getPageName().startsWith( config.wishIntakePage ) &&
			mw.config.get( 'wgAction' ) === 'view' &&
			!this.isWishEdit() &&
			// Don't load on diff pages
			!mw.config.get( 'wgDiffOldId' );
	}

	/**
	 * Are we currently viewing (but not editing) a wish page?
	 *
	 * @return {boolean}
	 */
	static isWishView() {
		return this.isWishPage() && !this.isWishEdit() && mw.config.get( 'wgAction' ) === 'view';
	}

	/**
	 * Are we currently editing a wish page?
	 *
	 * @return {boolean}
	 */
	static isWishEdit() {
		return this.isWishPage() && !!mw.util.getParamValue( config.wishEditParam );
	}

	/**
	 * Are we currently manually editing a wish page?
	 *
	 * @return {boolean}
	 */
	static isManualWishEdit() {
		return this.isWishPage() &&
			(
				mw.config.get( 'wgAction' ) === 'edit' ||
				document.documentElement.classList.contains( 've-active' )
			);
	}

	/**
	 * Are we currently on a focus area page?
	 *
	 * @return {boolean}
	 */
	static isFocusAreaPage() {
		return this.getPageName().startsWith( config.focusAreaPagePrefix );
	}

	/**
	 * Get the user's preferred language.
	 *
	 * @return {string}
	 */
	static userPreferredLang() {
		if ( mw.config.get( 'wgArticleId' ) === 0 ) {
			// Use interface language for new pages.
			return mw.config.get( 'wgUserLanguage' );
		}
		// Use content language for existing pages.
		return mw.config.get( 'wgContentLanguage' );
	}

	/**
	 * Is the user's preferred language right-to-left?
	 *
	 * @return {boolean}
	 */
	static isRtl() {
		return $( 'body' ).css( 'direction' ) === 'rtl';
	}

	/**
	 * Are we on a page related to creating, editing, or viewing a wish?
	 * This can include viewing the revision history, manual editing of wishes, etc.
	 *
	 * @return {boolean}
	 */
	static isWishRelatedPage() {
		return this.isNewWish() || this.isWishEdit() || this.isWishView() || this.isWishPage();
	}

	/**
	 * Should we show the intake form?
	 *
	 * @return {boolean}
	 */
	static shouldShowForm() {
		// Prevent form from loading on i.e. action=history
		return mw.config.get( 'wgAction' ) === 'view' &&
			( this.isNewWish() || this.isWishEdit() );
	}

	/**
	 * Get the slug for the wish derived from the page title.
	 * This is the subpage title and not necessarily the wish title,
	 * which is stored in the proposal content.
	 *
	 * @return {string|null} null if not a wish-related page
	 */
	static getWishSlug() {
		if ( this.isNewWish() ) {
			// New wishes have no slug yet.
			return '';
		} else if ( this.isWishPage() ) {
			// Existing wishes have the page prefix stripped.
			const slugPortion = this.getPageName().slice( config.wishPagePrefix.length );
			// Strip off language subpage. Slashes are disallowed in wish slugs.
			return slugPortion.split( '/' )[ 0 ];
		}
		return null;
	}

	/**
	 * Get the slug for the focus area derived from the page title.
	 *
	 * @return {string|null} null if not a focus area page
	 */
	static getFocusAreaSlug() {
		if ( this.isFocusAreaPage() ) {
			const slugPortion = this.getPageName().slice( config.focusAreaPagePrefix.length );
			// Strip off language subpage. Slashes are disallowed in focus area slugs.
			return slugPortion.split( '/' )[ 0 ];
		}
		return null;
	}

	/**
	 * Get the full page title of the wish from the slug.
	 *
	 * @param {string} slug
	 * @return {string}
	 */
	static getWishPageTitleFromSlug( slug ) {
		return config.wishPagePrefix + slug;
	}

	/**
	 * Is the user WMF staff?
	 *
	 * @todo WMF-specific
	 * @return {boolean}
	 */
	static isStaff() {
		return /\s\(WMF\)$|-WMF$/.test( mw.config.get( 'wgUserName' ) );
	}

	/**
	 * Log an error to the console.
	 *
	 * @param {string} text
	 * @param {Error} error
	 */
	static logError( text, error ) {
		mw.log.error( `[WishlistIntake] ${ text }`, error );
	}

	/**
	 * Get a CSS-only Codex Message component of the specified type.
	 * This is for use outside the Vue application.
	 *
	 * @param {mw.Message} message
	 * @param {string} type 'notice', 'warning', 'error' or 'success'
	 * @return {HTMLDivElement}
	 */
	static getMessageBox( message, type ) {
		const messageBlock = document.createElement( 'div' );
		// The following messages may be used here:
		// * cdx-message--notice
		// * cdx-message--warning
		// * cdx-message--error
		// * cdx-message--success
		messageBlock.classList.add( 'cdx-message', 'cdx-message--block', `cdx-message--${ type }` );
		if ( type === 'warning' ) {
			messageBlock.role = 'alert';
		} else {
			messageBlock.ariaLive = 'polite';
		}
		const icon = document.createElement( 'span' );
		icon.classList.add( 'cdx-message__icon' );
		const content = document.createElement( 'div' );
		content.classList.add( 'cdx-message__content' );
		content.innerHTML = message.parse();
		messageBlock.appendChild( icon );
		messageBlock.appendChild( content );
		return messageBlock;
	}

	/**
	 * Is the user viewing in mobile format?
	 *
	 * @return {boolean}
	 */
	static isMobile() {
		return !!mw.config.get( 'wgMFMode' );
	}

	/**
	 * Fetch messages from the wiki and set them in mw.messages.
	 *
	 * @param {mw.Api} api
	 * @return {jQuery.Promise}
	 */
	static setOnWikiMessages( api ) {
		const titles = [ config.messagesPage + '/en' ],
			langPageLocal = config.messagesPage + '/' + mw.config.get( 'wgUserLanguage' );

		if ( mw.config.get( 'wgUserLanguage' ) !== 'en' ) {
			titles.push( langPageLocal );
		}

		return api.get( {
			action: 'query',
			prop: 'revisions',
			titles,
			rvprop: 'content',
			rvslots: 'main',
			format: 'json',
			formatversion: 2,
			// Cache for 30 minutes.
			maxage: 1800,
			smaxage: 1800
		} ).then( ( resp ) => {
			let messagesLocal = {},
				messagesEn = {};
			/**
			 * The content model of the messages page is wikitext so that it can be used with
			 * Extension:Translate. Consequently, it's possible to break things. This just does
			 * a try/catch and returns the default English messages if it fails.
			 *
			 * @param {string} title
			 * @param {string} content
			 * @return {Object}
			 */
			const parseJSON = ( title, content ) => {
				try {
					return JSON.parse( content );
				} catch ( e ) {
					WebUtil.logError( `Failed to parse JSON for ${ title }.`, e );
					return { messages: config.messages };
				}
			};
			resp.query.pages.forEach( ( page ) => {
				if ( !page.revisions ) {
					// Missing
					return;
				}
				const pageObj = page.revisions[ 0 ].slots.main;
				const parsedContent = parseJSON( config.messagesPage, pageObj.content );

				if ( page.title === langPageLocal && mw.config.get( 'wgUserLanguage' ) !== 'en' ) {
					messagesLocal = parsedContent.messages;
				} else {
					messagesEn = parsedContent.messages;
				}
			} );

			mw.messages.set( Object.assign( messagesEn, messagesLocal ) );
		} );
	}
}

class DescriptionField {
	/**
	 * @param {HTMLElement|jQuery|string} node
	 * @param {string} content
	 */
	constructor( node, content ) {
		/**
		 * The textarea that will be replaced by the VisualEditor.
		 *
		 * @type {jQuery}
		 */
		this.$textarea = $( node );
		/**
		 * The wrapper of the textarea and the VisualEditor.
		 *
		 * @type {jQuery}
		 */
		this.$veWrapper = this.$textarea.parent();
		/**
		 * The MediaWiki target.
		 *
		 * @type {ve.init.mw.Target}
		 */
		this.target = null;
		/**
		 * The VisualEditor surface.
		 *
		 * @type {ve.ui.Surface}
		 */
		this.surface = null;
		/**
		 * The contents of the textarea/surface.
		 *
		 * @type {string} Always wikitext.
		 */
		this.content = content;
	}

	/**
	 * All dependencies required for our VisualEditor implementation.
	 *
	 * @return {string[]}
	 */
	get dependencies() {
		const dependencies = WebUtil.isMobile() ?
			[ 'ext.visualEditor.core.mobile' ] :
			[ 'ext.visualEditor.core.desktop' ];
		return dependencies.concat( [
			'ext.visualEditor.mwcore',
			'ext.visualEditor.mwwikitext',
			'ext.visualEditor.switching',
			'ext.visualEditor.desktopTarget',
			'ext.visualEditor.mwextensions',
			'oojs-ui-widgets',
			'oojs-ui.styles.indicators',
			'oojs-ui.styles.icons-editing-styling',
			'oojs-ui.styles.icons-editing-list',
			'mediawiki.ForeignStructuredUpload.BookletLayout',
			// Intentionally hand-picked PluginModules
			'ext.cite.visualEditor',
			'ext.citoid.visualEditor',
			'ext.translate.ve'
		] );
	}

	/**
	 * Get the current mode of the VisualEditor,
	 * or the default mode if the ve.ui.Surface is not initialized.
	 *
	 * @return {string} 'source' or 'visual'
	 */
	get mode() {
		return this.surface ? this.surface.getMode() : this.defaultMode;
	}

	/**
	 * Get the default mode of the VisualEditor.
	 *
	 * @return {string} 'source' or 'visual'
	 */
	get defaultMode() {
		return mw.user.options.get( 'visualeditor-editor' ) === 'visualeditor' ?
			'visual' :
			'source';
	}

	/**
	 * Initialize the VisualEditor.
	 */
	init() {
		// Add modes and other tools the toolbar registry.
		const { CwVisualEditModeTool, CwSourceEditModeTool } = this.getEditModeTools();
		ve.ui.toolFactory.register( CwVisualEditModeTool );
		ve.ui.toolFactory.register( CwSourceEditModeTool );
		ve.ui.toolFactory.register( ve.ui.MWCitationDialogTool );
		ve.ui.toolFactory.register( ve.ui.MWReferenceDialogTool );
		ve.ui.toolFactory.register( ve.ui.CitoidInspectorTool );

		ve.init.mw.Platform.static.initializedPromise
			.fail( () => {
				this.$veWrapper.text( 'Sorry, this browser is not supported.' );
			} )
			.done( this.createTarget.bind( this ) );
	}

	/**
	 * Get the VisualEditor edit mode tools, customized to switch between source/visual.
	 *
	 * @return {Object} { CwVisualEditModeTool, CwSourceEditModeTool }
	 */
	getEditModeTools() {
		const CwEditModeTool = function () {};
		OO.initClass( CwEditModeTool );
		OO.inheritClass( CwEditModeTool, mw.libs.ve.MWEditModeTool );
		CwEditModeTool.prototype.getMode = function () {
			if ( !this.toolbar.getSurface() ) {
				return 'source';
			}
			return this.toolbar.getSurface().getMode();
		};

		const CwVisualEditModeTool = function () {
			CwEditModeTool.super.apply( this, arguments );
			CwEditModeTool.call( this );
		};
		OO.inheritClass( CwVisualEditModeTool, mw.libs.ve.MWEditModeVisualTool );
		OO.mixinClass( CwVisualEditModeTool, CwEditModeTool );

		const CwSourceEditModeTool = function () {
			CwEditModeTool.super.apply( this, arguments );
			CwEditModeTool.call( this );
		};
		OO.inheritClass( CwSourceEditModeTool, mw.libs.ve.MWEditModeSourceTool );
		OO.mixinClass( CwSourceEditModeTool, CwEditModeTool );

		return {
			CwVisualEditModeTool,
			CwSourceEditModeTool
		};
	}

	/**
	 * Get the content of the Surface.
	 *
	 * @return {jQuery.Promise<string>} HTML or wikitext
	 */
	getWikitext() {
		return this.mode === 'source' ?
			ve.createDeferred().resolve( this.surface.getDom() ).promise() :
			this.target.getWikitextFragment( this.surface.getModel().getDocument() );
	}

	get toolbarGroups() {
		if ( WebUtil.isMobile() ) {
			// The following is identical to ve.init.mw.MobileArticleTarget.static.toolbarGroups.
			// We don't reference it because we don't want to load all of the
			// ext.visualEditor.mobileArticleTarget module and its dependencies.
			return [
				// History
				{
					name: 'history',
					include: [ 'undo' ]
				},
				// Style
				{
					name: 'style',
					classes: [ 've-test-toolbar-style' ],
					type: 'list',
					icon: 'textStyle',
					title: OO.ui.deferMsg( 'visualeditor-toolbar-style-tooltip' ),
					label: OO.ui.deferMsg( 'visualeditor-toolbar-style-tooltip' ),
					invisibleLabel: true,
					include: [ { group: 'textStyle' }, 'language', 'clear' ],
					forceExpand: [ 'bold', 'italic', 'clear' ],
					promote: [ 'bold', 'italic' ],
					demote: [ 'strikethrough', 'code', 'underline', 'language', 'clear' ]
				},
				// Link
				{
					name: 'link',
					include: [ 'link' ]
				}
			];
		}
		return ve.init.mw.Target.static.toolbarGroups;
	}

	/**
	 * Create the VisualEditor target and add initial content to the Surface.
	 *
	 * @param {string} mode 'source' or 'visual'
	 * @return {jQuery.Promise}
	 */
	createTarget( mode = this.defaultMode ) {
		this.target = new ve.init.mw.Target( {
			surfaceClasses: [ 'wishlist-intake-ve-surface' ],
			modes: [ 'visual', 'source' ],
			defaultMode: mode,
			toolbarConfig: { position: 'top' },
			toolbarGroups: [
				...this.toolbarGroups,
				{
					name: 'editMode',
					type: 'list',
					icon: 'edit',
					title: ve.msg( 'visualeditor-mweditmode-tooltip' ),
					label: ve.msg( 'visualeditor-mweditmode-tooltip' ),
					invisibleLabel: true,
					include: [ 'editModeVisual', 'editModeSource' ],
					align: WebUtil.isMobile() ? 'center' : 'after'
				}
			]
		} );

		// Listener for edit mode switch.
		this.target.getToolbar().on( 'switchEditor', this.switchEditor.bind( this ) );

		// Add initial content.
		return this.setSurface( this.content, mode )
			.then( () => {
				// Add the target to the document.
				this.$veWrapper.html( this.target.$element );
				this.setPending( false );
			} );
	}

	/**
	 * Switch the editor to the specified mode.
	 *
	 * @param {string} mode 'source' or 'visual'
	 * @return {jQuery.Promise}
	 */
	switchEditor( mode ) {
		if ( mode === this.mode ) {
			return ve.createDeferred().resolve().promise();
		}

		this.setPending( true );
		return this.getWikitext().then( ( content ) => {
			this.content = content;
		} ).then( () => {
			const oldTarget = this.target;
			return this.createTarget( mode ).then( () => {
				oldTarget.destroy();
				this.surface.focus();

				// Silently save preference.
				const editor = mode === 'source' ? 'wikitext' : 'visualeditor';
				new mw.Api().saveOption( 'visualeditor-editor', editor ).then( () => {
					mw.user.options.set( 'visualeditor-editor', editor );
				} );
			} );
		} );
	}

	/**
	 * Set the content of the Surface.
	 *
	 * @param {string} wikitext
	 * @param {string} mode 'source' or 'visual'
	 * @return {jQuery.Promise}
	 */
	setSurface( wikitext, mode ) {
		return this.getDocFromWikitext( wikitext, mode ).then( ( doc ) => {
			this.target.clearSurfaces();
			this.surface = this.target.addSurface( doc );
		} );
	}

	/**
	 * Create a document model from the given wikitext.
	 *
	 * @param {string} wikitext
	 * @param {string} mode 'source' or 'visual'
	 * @return {jQuery.Promise<ve.dm.Document>}
	 */
	getDocFromWikitext( wikitext, mode ) {
		const options = {
			lang: WebUtil.userPreferredLang(),
			dir: WebUtil.isRtl() ? 'rtl' : 'ltr'
		};
		if ( mode === 'source' ) {
			return ve.createDeferred().resolve(
				ve.dm.sourceConverter.getModelFromSourceText( wikitext, options )
			).promise();
		}

		const outerPromise = ve.createDeferred();

		// Transform the wikitext to HTML.
		this.target.parseWikitextFragment( wikitext ).then( ( resp ) => {
			const htmlDoc = this.target.parseDocument( resp.visualeditor.content );
			// Avoids issues like T253584 where IDs could clash.
			mw.libs.ve.stripRestbaseIds( htmlDoc );
			const doc = ve.dm.converter.getModelFromDom( htmlDoc, options );
			outerPromise.resolve( doc );
		} );

		return outerPromise;
	}

	/**
	 * Synchronize changes from the Surface to the textarea.
	 *
	 * @return {jQuery.Promise}
	 */
	syncChangesToTextarea() {
		return this.getWikitext().then( ( content ) => {
			this.$textarea.val( this.escapePipesInTables( content ) );
			// Propagate the change to the Vue model.
			this.$textarea[ 0 ].dispatchEvent( new Event( 'input' ) );
		} );
	}

	/**
	 * Escape pipes in tables where they may confuse the parser.
	 *
	 * This algorithm is far from perfect, but should be satisfactory in most cases.
	 * Known issues include:
	 * * Template calls within a table, and those calls include a pipe at the beginning of a line.
	 * * Complex or multiline use of <nowiki> or <pre>
	 *
	 * Some code adapted from Extension:VEForAll (GPL-2.0-or-later)
	 * See https://w.wiki/AVB5
	 *
	 * @param {string} wikitext
	 * @return {string}
	 */
	escapePipesInTables( wikitext ) {
		const lines = wikitext.split( '\n' );
		let withinTable = false;

		for ( let i = 0; i < lines.length; i++ ) {
			const curLine = lines[ i ];
			// start of table is {|, but could be also escaped, like this: {{{!}}
			if ( curLine.indexOf( '{|' ) === 0 || curLine.indexOf( '{{{!}}' ) === 0 ) {
				withinTable = true;
				lines[ i ] = curLine.replace( /\|/g, '{{!}}' );
			}
			if ( withinTable && ( curLine.indexOf( '|' ) === 0 || curLine.indexOf( '!' ) === 0 ) ) {
				lines[ i ] = curLine.replace( /\|/g, '{{!}}' );
			}
			// Table caption case (`|+`). See https://www.mediawiki.org/wiki/Help:Tables
			if ( withinTable && lines[ i ].includes( '|+' ) ) {
				lines[ i ] = curLine.replace( /\|\+/g, '{{!}}+' );
			}
			// colspan/rowspan case (`|rowspan=`/`|colspan=`). See https://www.mediawiki.org/wiki/Help:Tables
			if ( withinTable && ( curLine.includes( 'colspan' ) || curLine.includes( 'rowspan' ) ) ) {
				lines[ i ] = curLine.replace( /(colspan|rowspan)="(\d+?)"\s*\|/, '$1="$2" {{!}}' )
					.replace( /^\s*\|/, '{{!}} ' );
			}
			if ( withinTable ) {
				// Unescape pipes in <nowiki>, <pre> and in wiki links.
				const chunks = lines[ i ].match( /<nowiki>.*?<\/nowiki>|<pre>.*?<\/pre>|\[\[.*?]]/g ) || [];
				chunks.forEach( ( chunk ) => {
					lines[ i ] = lines[ i ].replace( chunk, chunk.replace( /\{\{!}}/g, '|' ) );
				} );
			}
			if ( curLine.indexOf( '|}' ) === 0 ) {
				withinTable = false;
			}
		}
		return lines.join( '\n' );
	}

	/**
	 * Mimic pending state.
	 *
	 * @param {boolean} pending
	 */
	setPending( pending ) {
		this.$textarea.prop( 'disabled', pending );
		this.$veWrapper.toggleClass( 'wishlist-intake-textarea-wrapper--loading', pending );
	}
}

// This must live here outside the Vue component to prevent Vue from interfering with VE.
let descriptionField;

var script$4 = vue.defineComponent( {
	name: 'DescriptionSection',
	components: {
		CdxField: codex.CdxField,
		CdxTextInput: codex.CdxTextInput
	},
	props: {
		title: { type: String, default: '' },
		description: { type: String, default: '' },
		titlestatus: { type: String, default: 'default' },
		descriptionstatus: { type: String, default: 'default' },
		disabled: { type: Boolean, default: false }
	},
	emits: [
		'update:title',
		'update:description',
		'update:pre-submit-promise'
	],
	data() {
		return {
			titlemessage: {},
			descriptionmessage: {}
		};
	},
	methods: {
		setupDescriptionField() {
			if ( descriptionField ) {
				return;
			}
			const textarea = document.querySelector( '.wishlist-intake-textarea' );
			descriptionField = new DescriptionField( textarea, this.description );
			descriptionField.setPending( true );
			return mw.loader.using( descriptionField.dependencies ).then( () => {
				descriptionField.init();
				this.$emit(
					'update:pre-submit-promise',
					descriptionField.syncChangesToTextarea.bind( descriptionField )
				);
			} );
		}
	},
	watch: {
		titlestatus: {
			handler( newStatus ) {
				if ( newStatus === 'error' ) {
					this.titlemessage = {
						error: mw.msg( 'communitywishlist-title-error', 5, 100 )
					};
				} else {
					this.titlemessage = {};
				}
			}
		},
		descriptionstatus: {
			handler( newStatus ) {
				if ( newStatus === 'error' ) {
					this.descriptionmessage = {
						error: mw.msg( 'communitywishlist-description-error', 50 )
					};
				} else {
					this.descriptionmessage = {};
				}
			}
		}
	},
	mounted() {
		this.setupDescriptionField();
	}
} );

const _hoisted_1$4 = { class: "wishlist-intake-description" };
const _hoisted_2$1 = { class: "wishlist-intake-textarea-wrapper wishlist-intake-textarea-wrapper--loading" };
const _hoisted_3$1 = ["value"];

function render$4(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_cdx_text_input = vue.resolveComponent("cdx-text-input");
  const _component_cdx_field = vue.resolveComponent("cdx-field");

  return (vue.openBlock(), vue.createElementBlock("section", _hoisted_1$4, [
    vue.createVNode(_component_cdx_field, {
      status: _ctx.titlestatus,
      messages: _ctx.titlemessage,
      disabled: _ctx.disabled,
      class: "community-wishlist-title-field"
    }, {
      label: vue.withCtx(() => [
        vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-title' ).text()), 1 /* TEXT */)
      ]),
      description: vue.withCtx(() => [
        vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-title-description' ).text()), 1 /* TEXT */)
      ]),
      default: vue.withCtx(() => [
        vue.createVNode(_component_cdx_text_input, {
          "model-value": _ctx.title,
          onInput: _cache[0] || (_cache[0] = $event => (_ctx.$emit( 'update:title', $event.target.value.trim() )))
        }, null, 8 /* PROPS */, ["model-value"])
      ]),
      _: 1 /* STABLE */
    }, 8 /* PROPS */, ["status", "messages", "disabled"]),
    vue.createVNode(_component_cdx_field, {
      status: _ctx.descriptionstatus,
      messages: _ctx.descriptionmessage,
      disabled: _ctx.disabled,
      class: "community-wishlist-description-field"
    }, {
      label: vue.withCtx(() => [
        vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-description' ).text()), 1 /* TEXT */)
      ]),
      description: vue.withCtx(() => [
        vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-description-description' ).text()), 1 /* TEXT */)
      ]),
      default: vue.withCtx(() => [
        vue.createElementVNode("div", _hoisted_2$1, [
          vue.createElementVNode("textarea", {
            class: "wishlist-intake-textarea",
            rows: 8,
            value: _ctx.description,
            onInput: _cache[1] || (_cache[1] = $event => (_ctx.$emit( 'update:description', $event.target.value.trim() )))
          }, "\n\t\t\t\t", 40 /* PROPS, NEED_HYDRATION */, _hoisted_3$1)
        ])
      ]),
      _: 1 /* STABLE */
    }, 8 /* PROPS */, ["status", "messages", "disabled"])
  ]))
}

__$styleInject("/* Overrides to make OOUI sort of mimic Codex */\n.wishlist-intake-textarea-wrapper {\n  border: 1px solid #a2a9b1;\n  /* TODO: Replace with official Codex loading styles once established. */\n  /* See https://w.wiki/AHp3 */\n}\n.wishlist-intake-textarea-wrapper .ve-ui-surface-placeholder,\n.wishlist-intake-textarea-wrapper .ve-ui-surface .ve-ce-attachedRootNode {\n  padding: 0.5em 1em;\n}\n.wishlist-intake-textarea-wrapper .ve-ce-surface .ve-ce-attachedRootNode {\n  min-height: 194px;\n}\n.wishlist-intake-textarea-wrapper .wishlist-intake-ve-surface {\n  transition-property: background-color, color, border-color, box-shadow;\n  /* XXX: doesn't appear to be a Codex token for this */\n  transition-duration: 0.25s;\n}\n.wishlist-intake-textarea-wrapper .wishlist-intake-ve-surface:has( > .ve-ce-surface-focused ) {\n  border: 1px solid #36c;\n  box-sizing: border-box;\n  box-shadow: inset 0 0 0 1px #36c;\n}\n.wishlist-intake-textarea-wrapper--loading {\n  background-color: #eaecf0;\n  background-image: linear-gradient(135deg, #fff 25%, transparent 25%, transparent 50%, #fff 50%, #fff 75%, transparent 75%, transparent);\n  background-size: 1.25em 1.25em;\n  animation-name: cdx-animation-pending-stripes;\n  animation-duration: 650ms;\n  animation-timing-function: linear;\n  animation-iteration-count: infinite;\n}\n@keyframes cdx-animation-pending-stripes {\n  0% {\n    background-position: -1.25em 0;\n  }\n  100% {\n    background-position: 0 0;\n  }\n}\n.cdx-field--disabled .wishlist-intake-textarea-wrapper {\n  opacity: 0.5;\n  pointer-events: none;\n}\n.skin-minerva .wishlist-intake-textarea {\n  box-sizing: border-box;\n  width: 100%;\n}\n");

script$4.render = render$4;

var script$3 = vue.defineComponent( {
	name: 'AudienceSection',
	components: {
		CdxField: codex.CdxField,
		CdxTextInput: codex.CdxTextInput
	},
	props: {
		audience: { type: String, default: '' },
		status: { type: String, default: 'default' },
		disabled: { type: Boolean, default: false }
	},
	emits: [
		'update:audience'
	],
	data() {
		return {
			messages: {}
		};
	},
	watch: {
		status: {
			handler( newStatus ) {
				if ( newStatus === 'error' ) {
					this.messages = {
						error: mw.msg( 'communitywishlist-audience-error', 5, 300 )
					};
				} else {
					this.messages = {};
				}
			}
		}
	}
} );

const _hoisted_1$3 = { class: "wishlist-intake-audience" };

function render$3(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_cdx_text_input = vue.resolveComponent("cdx-text-input");
  const _component_cdx_field = vue.resolveComponent("cdx-field");

  return (vue.openBlock(), vue.createElementBlock("section", _hoisted_1$3, [
    vue.createVNode(_component_cdx_field, {
      status: _ctx.status,
      messages: _ctx.messages,
      disabled: _ctx.disabled
    }, {
      label: vue.withCtx(() => [
        vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-audience-label' ).text()), 1 /* TEXT */)
      ]),
      description: vue.withCtx(() => [
        vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-audience-description' ).text()), 1 /* TEXT */)
      ]),
      default: vue.withCtx(() => [
        vue.createVNode(_component_cdx_text_input, {
          "model-value": _ctx.audience,
          onInput: _cache[0] || (_cache[0] = $event => (_ctx.$emit( 'update:audience', $event.target.value.trim() )))
        }, null, 8 /* PROPS */, ["model-value"])
      ]),
      _: 1 /* STABLE */
    }, 8 /* PROPS */, ["status", "messages", "disabled"])
  ]))
}

script$3.render = render$3;

/**
 * This component accepts and provides a comma-separated wikitext list of
 * Phabricator links, and handles all splitting, combining, and normalization
 * itself.
 */
var script$2 = vue.defineComponent( {
	name: 'PhabricatorTasks',
	components: {
		CdxField: codex.CdxField,
		CdxChipInput: codex.CdxChipInput
	},
	props: {
		tasks: { type: Array, default: () => [] },
		disabled: { type: Boolean, default: false }
	},
	emits: [
		'update:tasks'
	],
	data( props ) {
		return {
			// An array of ChipInputItems containing Phabricator IDs.
			taskList: this.arrayToChipItems( props.tasks )
		};
	},
	methods: {
		/**
		 * Uppercase and sort an array of task IDs.
		 * @param {Array<string>} taskIds
		 * @return {Array<string>}
		 */
		normalizeTaskIds( taskIds ) {
			const allTaskIds = Array.prototype.concat( ...taskIds.map( ( taskId ) => {
				// One taskId might actually contain multiple,
				// e.g. if the user doesn't put a space between them.
				const currentTaskIds = taskId.match( /[Tt][0-9]+/g ) || [];
				return currentTaskIds.map( ( t ) => t.toUpperCase() );
			} ) );
			// Filter to be unique, and sort.
			return allTaskIds.filter( ( v, i, a ) => a.indexOf( v ) === i ).sort();
		},
		updateInputChips( chips ) {
			const taskIds = this.normalizeTaskIds( chips.map( ( c ) => c.value ) );
			this.$emit( 'update:tasks', taskIds );
		},
		arrayToChipItems( array ) {
			return array.map( ( t ) => {
				return { value: t };
			} );
		}
	},
	watch: {
		tasks: {
			handler( newVal ) {
				this.taskList = this.arrayToChipItems( newVal );
			},
			deep: true
		}
	}
} );

const _hoisted_1$2 = { class: "wishlist-intake-tasks" };

function render$2(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_cdx_chip_input = vue.resolveComponent("cdx-chip-input");
  const _component_cdx_field = vue.resolveComponent("cdx-field");

  return (vue.openBlock(), vue.createElementBlock("section", _hoisted_1$2, [
    vue.createVNode(_component_cdx_field, { disabled: _ctx.disabled }, {
      label: vue.withCtx(() => [
        vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-phabricator-label' ).text()), 1 /* TEXT */)
      ]),
      description: vue.withCtx(() => [
        vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-phabricator-desc' ).text()), 1 /* TEXT */)
      ]),
      default: vue.withCtx(() => [
        vue.createVNode(_component_cdx_chip_input, {
          "input-chips": _ctx.taskList,
          "onUpdate:inputChips": [
            _cache[0] || (_cache[0] = $event => ((_ctx.taskList) = $event)),
            _ctx.updateInputChips
          ],
          "chip-aria-description": _ctx.$i18n( 'communitywishlist-phabricator-chip-desc' ).text()
        }, null, 8 /* PROPS */, ["input-chips", "chip-aria-description", "onUpdate:inputChips"])
      ]),
      _: 1 /* STABLE */
    }, 8 /* PROPS */, ["disabled"])
  ]))
}

script$2.render = render$2;

class TemplateParserError extends Error {
}

/**
 * Extension tag names from metawiki
 *
 * TODO: fetch this list from the wiki being acted on
 *
 * @type {string[]}
 */
const EXT_NAMES = [
	'pre',
	'nowiki',
	'gallery',
	'indicator',
	'langconvert',
	'graph',
	'languages',
	'timeline',
	'hiero',
	'charinsert',
	'ref',
	'references',
	'inputbox',
	'imagemap',
	'source',
	'syntaxhighlight',
	'poem',
	'categorytree',
	'section',
	'score',
	'dynamicpagelist',
	'rss',
	'templatestyles',
	'templatedata',
	'math',
	'ce',
	'chem',
	'maplink',
	'mapframe',
	'phonos'
];

class TemplateParser {
	/**
	 * @callback TemplateCallback
	 * @param {string} name
	 * @param {Object<string>} args
	 */

	/**
	 * Parse some wikitext and call a function for each template.
	 *
	 * Throw a TemplateParserError if the input text does not match our
	 * restricted grammar.
	 *
	 * @param {string} text
	 * @param {TemplateCallback} callback
	 */
	static parse( text, callback ) {
		const parser = new TemplateParser( text, callback );
		parser.execute();
	}

	/**
	 * Parse some wikitext and extract the parameters of the first template
	 * with a name matching the specified name.
	 *
	 * If the template was not found, or if there was a parse error, return
	 * null.
	 *
	 * @param {string} text
	 * @param {string} targetTemplateName
	 * @return {?Object<string>}
	 */
	static getParams( text, targetTemplateName ) {
		function normalize( name ) {
			name = name.charAt( 0 ).toUpperCase() +
				name.slice( 1 );
			return name.replaceAll( '_', ' ' );
		}

		targetTemplateName = normalize( targetTemplateName );
		let foundParams = null;
		try {
			TemplateParser.parse(
				text,
				function ( templateName, args ) {
					if ( foundParams === null &&
						normalize( templateName ) === targetTemplateName
					) {
						foundParams = args;
					}
				}
			);
		} catch ( e ) {
			if ( !( e instanceof TemplateParserError ) ) {
				throw e;
			}
		}
		return foundParams;
	}

	/**
	 * @param {string} text
	 * @param {TemplateCallback} callback
	 */
	constructor( text, callback ) {
		this.text = text;
		this.callback = callback;
	}

	/**
	 * Run the parser
	 */
	execute() {
		this.consumeWikitext( 0, [] );
	}

	/**
	 * Consume the top-level grammar rule
	 *
	 * @param {number} pos The offset at which to begin parsing
	 * @param {string[]} terminators Markup which, if encountered, causes
	 *   the function to return
	 * @return {number} The new offset beyond the end
	 */
	consumeWikitext( pos, terminators ) {
		while ( pos < this.text.length ) {
			for ( const terminator of terminators ) {
				if ( this.text.startsWith( terminator, pos ) ) {
					return pos;
				}
			}

			const char = this.text.charAt( pos );
			const char2 = this.text.slice( pos, pos + 2 );
			if ( char2 === '}}' ) {
				break;
			} else if ( char2 === '{{' ) {
				pos = this.consumeTemplate( pos );
			} else if ( char2 === '[[' ) {
				pos = this.consumeLink( pos );
			} else if ( char2 === '<!' &&
				this.text.slice( pos, pos + 4 ) === '<!--'
			) {
				pos = this.consumeComment( pos );
			} else if ( char === '<' &&
				EXT_NAMES.includes( this.getExtName( pos + 1 ) )
			) {
				pos = this.consumeExtension( pos );
			} else {
				pos = this.consumeLiteral( pos );
			}
		}
		return pos;
	}

	/**
	 * Consume a template call
	 *
	 * @param {number} pos The offset of the start markup "{{"
	 * @return {number} The offset beyond the end
	 */
	consumeTemplate( pos ) {
		let nextArgIndex = 1;
		pos = this.consumeMarkup( pos, '{{' );
		const nameStart = pos;
		pos = this.consumeWikitext( pos, [ '|', '}}' ] );
		const templateName = this.text.slice( nameStart, pos ).trim();
		const templateParams = {};
		while ( pos < this.text.length && this.text.charAt( pos ) === '|' ) {
			pos = this.consumeMarkup( pos, '|' );
			const partStart = pos;
			let name, value;
			pos = this.consumeWikitext( pos, [ '=', '|', '}}' ] );
			if ( this.text.charAt( pos ) === '=' ) {
				name = this.text.slice( partStart, pos ).trim();
				pos++;
				const valueStart = pos;
				pos = this.consumeWikitext( pos, [ '|', '}}' ] );
				value = this.text.slice( valueStart, pos ).trim();
			} else {
				name = nextArgIndex++;
				value = this.text.slice( partStart, pos );
			}
			templateParams[ name ] = value;
		}
		this.callback( templateName, templateParams );
		pos = this.consumeMarkup( pos, '}}' );
		return pos;
	}

	/**
	 * Consume a link
	 *
	 * @param {number} pos The offset of the start markup "[["
	 * @return {number} The offset beyond the end
	 */
	consumeLink( pos ) {
		pos = this.consumeMarkup( pos, '[[' );
		pos = this.consumeWikitext( pos, [ ']]' ] );
		pos = this.consumeMarkup( pos, ']]' );
		return pos;
	}

	/**
	 * Consume an HTML comment
	 *
	 * @param {number} pos The offset of the start markup "<!--"
	 * @return {number} The offset beyond the end
	 */
	consumeComment( pos ) {
		pos = this.consumeMarkup( pos, '<!--' );
		const endPos = this.text.indexOf( '-->', pos );
		if ( endPos === -1 ) {
			this.error( pos, 'missing comment terminator' );
		}
		pos = endPos + '-->'.length;
		return pos;
	}

	/**
	 * Consume an xmlish extension element
	 *
	 * @param {number} pos The offset of the start markup
	 * @return {number} The offset beyond the end
	 */
	consumeExtension( pos ) {
		pos = this.consumeMarkup( pos, '<' );
		const name = this.getExtName( pos );
		pos += name.length;
		const tagEndPos = this.text.indexOf( '>', pos );
		if ( tagEndPos === -1 ) {
			this.error( pos, 'missing end of extension tag' );
		}
		pos = tagEndPos + 1;
		if ( this.text.charAt( tagEndPos - 1 ) === '/' ) {
			// Self-closing
			return pos;
		} else {
			const endTag = `</${ name }>`;
			const endPos = this.text.indexOf( endTag, pos );
			if ( endPos === -1 ) {
				this.error( pos, 'missing extension end tag' );
			}
			return endPos + endTag.length;
		}
	}

	/**
	 * Consume at least one literal character
	 *
	 * @param {number} pos
	 * @return {number} The offset beyond the end of the run of literal characters
	 */
	consumeLiteral( pos ) {
		const literal = this.text.slice( pos ).match( /^[^[{|=}\]<]*/ )[ 0 ];
		if ( literal.length ) {
			// Literal composed of uninteresting characters
			return pos + literal.length;
		}
		if ( pos < this.text.length ) {
			// Literal composed of one interesting character not consumed elsewhere
			return pos + 1;
		}
		return pos;
	}

	/**
	 * Assert that the specified characters exist at the specified location
	 *
	 * @param {number} pos
	 * @param {string} markup
	 * @return {number} The position beyond the end of the markup
	 */
	consumeMarkup( pos, markup ) {
		if ( this.text.slice( pos, pos + markup.length ) !== markup ) {
			this.error( pos, `expected "${ markup }"` );
		}
		return pos + markup.length;
	}

	/**
	 * Look ahead to find the name of a prospective xmlish extension tag
	 *
	 * @param {number} pos
	 * @return {string}
	 */
	getExtName( pos ) {
		return this.text.slice( pos ).match( /^[a-zA-Z0-9_-]*/ )[ 0 ];
	}

	/**
	 * Raise a parse error
	 *
	 * @param {number} pos
	 * @param {string} message
	 */
	error( pos, message ) {
		throw new TemplateParserError( `Syntax error parsing template at offset ${ pos }: ${ message }` );
	}
}

class WishlistTemplate {

	constructor( templateName ) {
		this.templateName = templateName.replace( /^Template:/, '' );
	}

	/**
	 * @param {Object} wish
	 * @return {string}
	 */
	getWikitext( wish ) {
		const paramNames = [
			'status',
			'type',
			'title',
			'description',
			'audience',
			'tasks',
			'proposer',
			'created',
			'projects',
			'otherproject',
			'area',
			'baselang'
		];
		let out = '{{' + this.templateName + '\n';
		for ( const key of paramNames ) {
			const value = wish[ key ];
			if ( value === '' ) {
				out += `| ${ key } =\n`;
			} else if ( value !== undefined ) {
				out += `| ${ key } = ${ value }\n`;
			}
		}
		out += '}}';
		return out;
	}

	/**
	 * Get the Wish object from the given wikitext, or null if the wish template
	 * was not found on the page. Any <translate> tags in the wikitext will be
	 * removed.
	 *
	 * @param {string} wikitext
	 * @param {string} pageTitle
	 * @param {?number} pageId
	 * @param {string} updated
	 * @return {?Wish}
	 */
	getWish( wikitext, pageTitle = '', pageId = null, updated = '' ) {
		const data = TemplateParser.getParams( wikitext, this.templateName );
		if ( data === null ) {
			return null;
		}
		data.page = pageTitle;
		if ( pageId !== null ) {
			data.pageId = pageId;
		}
		if ( pageTitle.startsWith( config.wishPagePrefix ) ) {
			const relPage = pageTitle.slice( config.wishPagePrefix.length );
			const m = relPage.match( /(.*)\/([a-z0-9-]{2,})$/ );
			if ( m ) {
				data.name = m[ 1 ];
				data.lang = m[ 2 ];
			} else {
				data.name = relPage;
				data.lang = '';
			}
		} else {
			data.name = pageTitle;
		}
		data.updated = updated;
		return new Wish( data );
	}

	/**
	 * Strip <translate> tags from a string
	 *
	 * @param {string} text
	 * @return {string}
	 */
	stripTranslate( text ) {
		text = text.replace( /<translate( nowrap)?>\n?/g, '' );
		text = text.replace( /\n?<\/translate>/g, '' );
		text = text.replace( /<tvar\s+name\s*=\s*(('[^']*')|("[^"]*")|([^"'\s>]*))\s*>.*?<\/tvar\s*>/g, '' );
		text = text.replace( /(^=.*=) <!--T:[^_/\n<>]+-->$/mg, '$1' );
		text = text.replace( /<!--T:[^_/\n<>]+-->[\n ]?/g, '' );
		return text;
	}
}

/**
 * Utility functions for the gadget and bot.
 */
class Util {
	/**
	 * Get the wish template object.
	 *
	 * @return {WishlistTemplate}
	 */
	static getWishTemplate() {
		return new WishlistTemplate( config.wishTemplate );
	}
}

const api$1 = new mw.Api();

// Functions returning Promises that must be resolved before the form can be validated/submitted.
// This is outside the Vue component as it does not need to be reactive.
const preSubmitFns = [];

var script$1 = vue.defineComponent( {
	name: 'WishlistIntake',
	components: {
		CdxMessage: codex.CdxMessage,
		StatusSection: script$7,
		WishTypeSection: script$6,
		ProjectSection: script$5,
		DescriptionSection: script$4,
		AudienceSection: script$3,
		PhabricatorTasks: script$2,
		CdxButton: codex.CdxButton
	},
	props: {
		audience: { type: String, default: '' },
		baselang: { type: String, default: mw.config.get( 'wgUserLanguage' ) },
		created: { type: String, default: '~~~~~' },
		description: { type: String, default: '' },
		otherproject: { type: String, default: '' },
		projects: { type: Array, default: () => [] },
		proposer: { type: String, default: '~~~' },
		status: { type: String, default: Wish.STATUS_SUBMITTED },
		tasks: { type: Array, default: () => [] },
		title: { type: String, default: '' },
		type: { type: String, default: null },
		area: { type: String, default: '' },
		// These are from the initial fetch of wish content,
		// and are later used for edit conflict detection on form submission.
		basetimestamp: { type: String, default: '' },
		curtimestamp: { type: String, default: '' }
	},
	setup( props ) {
		// Add a class to the body so the peer gadget styles become hidden.
		document.body.classList.add( 'wishlist-intake-ready' );
		// Customize the subtitle.
		document.querySelector( '#mw-content-subtitle' ).textContent = mw.msg( 'communitywishlist-form-subtitle' );

		// Reactive state for the form fields.
		// This should map directly to properties of the Wish class.
		const wish = vue.reactive( {
			audience: props.audience,
			baselang: props.baselang,
			created: props.created,
			description: props.description,
			otherproject: props.otherproject,
			projects: props.projects,
			proposer: props.proposer,
			status: props.status,
			tasks: props.tasks,
			title: props.title,
			type: props.type,
			area: props.area
		} );

		return { wish };
	},
	data() {
		return {
			exists: false,
			pagetitle: '',
			typeStatus: 'default',
			projectStatus: 'default',
			projectStatusType: 'default',
			titleStatus: 'default',
			descriptionStatus: 'default',
			audienceStatus: 'default',
			/**
			 * Whether the form has been changed since it was loaded.
			 * @type {boolean}
			 */
			formChanged: false,
			/**
			 * Disabled state of the form fields and submit button.
			 * @type {boolean}
			 */
			formDisabled: false,
			/**
			 * Error state of the form. Either false (no error), true (generic error),
			 * or a string with a specific error message.
			 * @type {boolean|string}
			 */
			formError: false,
			/**
			 * Whether the form has been submitted.
			 * @type {boolean}
			 */
			formSubmitted: false,
			/**
			 * @type {ConfirmCloseWindow}
			 */
			allowCloseWindow: mw.confirmCloseWindow(),
			/**
			 * URL to return to after the form is submitted.
			 * @type {string}
			 */
			returnto: mw.util.getUrl( config.wishHomePage )
		};
	},
	computed: {
		isStaff: WebUtil.isStaff,
		formErrorMsg: () => mw.message( 'communityrequests-form-error', config.wishHomePage ).parse()
	},
	methods: {
		updateField( field, value ) {
			if ( this.wish[ field ] !== value ) {
				this.wish[ field ] = value;
				this.formChanged = true;
			}
		},
		updateTitle( title ) {
			this.updateField( 'title', title );
			if ( title && !this.exists ) {
				// FIXME: this code may not be needed anymore?
				// If this is a new proposal, keep the new page title in sync with the title field.
				// Existing proposals can't change their page title via the form (but can change the
				// title field).
				title = mw.Title.newFromUserInput( this.wish.title.replaceAll( '/', '_' ) );
				this.pagetitle = WebUtil.getWishPageTitleFromSlug( title.getMainText() );
			}
			// VisualEditor and other scripts rely on this being the correct post-save page name.
			// If the wish title is blank, we use the current page name as a fallback.
			mw.config.set(
				'wgRelevantPageName',
				!this.wish.title ? WebUtil.getPageName() : this.pagetitle
			);
		},
		/**
		 * Validate the form fields.
		 *
		 * @return {boolean} true if the form is valid, false otherwise.
		 */
		validateForm() {
			this.formError = false;
			// Remove translate tags before checking title length.
			const title = this.wish.title
				.replaceAll( /<\/?translate>/g, '' )
				.replaceAll( /<!--T:[0-9]+-->/g, '' );
			this.titleStatus = ( title.length < 5 || title.length > 100 ) ? 'error' : 'default';
			this.descriptionStatus = ( this.wish.description.length < 50 ) ? 'error' : 'default';
			this.typeStatus = this.wish.type === null ? 'error' : 'default';
			// No project selected, other project is empty
			if ( this.wish.projects.length === 0 && !this.wish.otherproject ) {
				this.projectStatus = 'error';
				this.projectStatusType = 'noSelection';
			// Other project has content > 3, but no other project is entered
			} else if ( this.wish.otherproject.length < 3 && this.wish.projects.length < 1 ) {
				this.projectStatus = 'error';
				this.projectStatusType = 'invalidOther';
			} else {
				this.projectStatus = 'default';
				this.projectStatusType = 'default';
			}
			this.audienceStatus = ( this.wish.audience.length < 5 || this.wish.audience.length > 300 ) ? 'error' : 'default';
			return this.typeStatus !== 'error' &&
				this.titleStatus !== 'error' &&
				this.descriptionStatus !== 'error' &&
				this.audienceStatus !== 'error' &&
				this.projectStatus !== 'error';
		},
		/**
		 * Add a function to be called before the form is submitted.
		 * The function is expected to return a Promise, which is
		 * guaranteed to be resolved before the form is submitted.
		 *
		 * @param {Function<Promise|jQuery.Promise>} fn
		 */
		addPreSubmitFn( fn ) {
			preSubmitFns.push( fn );
		},
		/**
		 * Get a unique page title by appending incremental numbers to the end of the title.
		 *
		 * @param {string} pageTitle
		 * @param {number} [pageCounter]
		 * @return {jQuery.Promise<string>|Promise<string>}
		 */
		getUniquePageTitle( pageTitle, pageCounter = 1 ) {
			// A wish being edited is always going to already have a unique title.
			if ( WebUtil.isWishEdit() ) {
				return Promise.resolve( pageTitle );
			}
			// Otherwise, see if it exists and start appending numbers.
			const newTitle = pageTitle + ( pageCounter > 1 ? ' ' + pageCounter : '' );
			return this.pageExists( newTitle ).then( ( exists ) => {
				if ( exists ) {
					return this.getUniquePageTitle( pageTitle, pageCounter + 1 );
				} else {
					return Promise.resolve( newTitle );
				}
			} );
		},
		/**
		 * Get a promise for saving the wish page.
		 *
		 * @param {string} wikitext
		 * @return {jQuery.Promise}
		 */
		getEditPromise( wikitext ) {
			const params = api$1.assertCurrentUser( {
				action: 'edit',
				title: this.pagetitle,
				text: wikitext,
				formatversion: 2,
				// Tag the edit to track usage of the form.
				tags: [ 'community-wishlist' ],
				// Protect against conflicts
				basetimestamp: this.basetimestamp,
				starttimestamp: this.curtimestamp,
				// Localize errors
				uselang: mw.config.get( 'wgUserLanguage' ),
				errorformat: 'html',
				errorlang: mw.config.get( 'wgUserLanguage' ),
				errorsuselocal: true
			} );
			if ( WebUtil.isNewWish() ) {
				params.createonly = true;
			} else {
				params.nocreate = true;
			}
			return api$1.postWithEditToken( params );
		},
		/**
		 * Handle form submission.
		 */
		handleSubmit() {
			this.formDisabled = true;
			Promise.all( preSubmitFns.map( ( p ) => p() ) ).then( () => {
				// @todo Handle this nicer?
				if ( !this.validateForm() ) {
					this.formDisabled = false;
					return;
				}

				// Save the wish page, first checking for duplicate titles.
				this.getUniquePageTitle( this.pagetitle ).then( ( uniqueTitle ) => {
					this.pagetitle = uniqueTitle;
					const wikitext = Util.getWishTemplate().getWikitext( new Wish( this.wish ) );
					this.getEditPromise( wikitext ).then( ( saveResult ) => {
						if ( saveResult.edit && !saveResult.edit.nochange ) {
							// Replicate what is done in postEdit's fireHookOnPageReload() function,
							// but for a different page.
							mw.storage.session.set(
								// Same storage key structure as in MediaWiki's
								// resources/src/mediawiki.action/mediawiki.action.view.postEdit.js
								'mw-PostEdit' + this.pagetitle.replaceAll( ' ', '_' ),
								WebUtil.isWishEdit() ? 'saved' : 'created',
								// Same duration as EditPage::POST_EDIT_COOKIE_DURATION.
								1200
							);
						}
						this.formSubmitted = true; // @todo Unused variable?
						// Allow the window to close/navigate after submission was successful.
						this.allowCloseWindow.release();
						window.location.replace( mw.util.getUrl( this.pagetitle ) );
					} ).fail( this.handleError.bind( this ) );
				} );
			} ).catch( this.handleError.bind( this ) );
		},
		/**
		 * Handle an error from the API.
		 *
		 * @param {Error} errObj
		 * @param {Object} error Response from the API
		 * @param {string} error.info
		 */
		handleError( errObj, error ) {
			WebUtil.logError( 'edit failed', errObj );
			this.formError = api$1.getErrorMessage( error ).html();
			this.formDisabled = false;
		},
		/**
		 * Check if a page exists.
		 *
		 * @param {string} title
		 * @return {Promise<boolean>}
		 */
		pageExists( title ) {
			return api$1.get( {
				action: 'query',
				format: 'json',
				titles: title
			} ).then( ( res ) => Promise.resolve( res.query.pages[ -1 ] === undefined ) );
		}
	},
	beforeMount() {
		this.pagetitle = WebUtil.getWishPageTitleFromSlug( WebUtil.getWishSlug() );
		if ( WebUtil.isNewWish() ) {
			this.exists = false;
		} else {
			this.returnto = mw.util.getUrl( this.pagetitle );
			this.exists = true;
		}
	}
} );

const _hoisted_1$1 = { class: "wishlist-intake-form-footer" };
const _hoisted_2 = /*#__PURE__*/vue.createElementVNode("hr", null, null, -1 /* HOISTED */);
const _hoisted_3 = ["innerHTML"];
const _hoisted_4 = { key: 0 };
const _hoisted_5 = { key: 1 };
const _hoisted_6 = ["href"];
const _hoisted_7 = ["innerHTML"];
const _hoisted_8 = ["innerHTML"];

function render$1(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_status_section = vue.resolveComponent("status-section");
  const _component_description_section = vue.resolveComponent("description-section");
  const _component_wish_type_section = vue.resolveComponent("wish-type-section");
  const _component_project_section = vue.resolveComponent("project-section");
  const _component_audience_section = vue.resolveComponent("audience-section");
  const _component_phabricator_tasks = vue.resolveComponent("phabricator-tasks");
  const _component_cdx_button = vue.resolveComponent("cdx-button");
  const _component_cdx_message = vue.resolveComponent("cdx-message");

  return (vue.openBlock(), vue.createElementBlock("form", {
    onSubmit: _cache[15] || (_cache[15] = vue.withModifiers((...args) => (_ctx.handleSubmit && _ctx.handleSubmit(...args)), ["prevent"]))
  }, [
    (_ctx.isStaff)
      ? (vue.openBlock(), vue.createBlock(_component_status_section, {
          key: 0,
          status: _ctx.wish.status,
          "onUpdate:status": [
            _cache[0] || (_cache[0] = $event => ((_ctx.wish.status) = $event)),
            _cache[1] || (_cache[1] = $event => (_ctx.updateField( 'status', $event )))
          ],
          disabled: _ctx.formDisabled
        }, null, 8 /* PROPS */, ["status", "disabled"]))
      : vue.createCommentVNode("v-if", true),
    vue.createVNode(_component_description_section, {
      title: _ctx.wish.title,
      "onUpdate:title": [
        _cache[2] || (_cache[2] = $event => ((_ctx.wish.title) = $event)),
        _ctx.updateTitle
      ],
      description: _ctx.wish.description,
      "onUpdate:description": [
        _cache[3] || (_cache[3] = $event => ((_ctx.wish.description) = $event)),
        _cache[4] || (_cache[4] = $event => (_ctx.updateField( 'description', $event )))
      ],
      titlestatus: _ctx.titleStatus,
      descriptionstatus: _ctx.descriptionStatus,
      disabled: _ctx.formDisabled,
      "onUpdate:preSubmitPromise": _ctx.addPreSubmitFn
    }, null, 8 /* PROPS */, ["title", "description", "titlestatus", "descriptionstatus", "disabled", "onUpdate:title", "onUpdate:preSubmitPromise"]),
    vue.createVNode(_component_wish_type_section, {
      type: _ctx.wish.type,
      "onUpdate:type": [
        _cache[5] || (_cache[5] = $event => ((_ctx.wish.type) = $event)),
        _cache[6] || (_cache[6] = $event => (_ctx.updateField( 'type', $event )))
      ],
      status: _ctx.typeStatus,
      disabled: _ctx.formDisabled
    }, null, 8 /* PROPS */, ["type", "status", "disabled"]),
    vue.createVNode(_component_project_section, {
      projects: _ctx.wish.projects,
      "onUpdate:projects": [
        _cache[7] || (_cache[7] = $event => ((_ctx.wish.projects) = $event)),
        _cache[9] || (_cache[9] = $event => (_ctx.updateField( 'projects', $event )))
      ],
      otherproject: _ctx.wish.otherproject,
      "onUpdate:otherproject": [
        _cache[8] || (_cache[8] = $event => ((_ctx.wish.otherproject) = $event)),
        _cache[10] || (_cache[10] = $event => (_ctx.updateField( 'otherproject', $event )))
      ],
      disabled: _ctx.formDisabled,
      status: _ctx.projectStatus,
      statustype: _ctx.projectStatusType
    }, null, 8 /* PROPS */, ["projects", "otherproject", "disabled", "status", "statustype"]),
    vue.createVNode(_component_audience_section, {
      audience: _ctx.wish.audience,
      "onUpdate:audience": [
        _cache[11] || (_cache[11] = $event => ((_ctx.wish.audience) = $event)),
        _cache[12] || (_cache[12] = $event => (_ctx.updateField( 'audience', $event )))
      ],
      status: _ctx.audienceStatus,
      disabled: _ctx.formDisabled
    }, null, 8 /* PROPS */, ["audience", "status", "disabled"]),
    vue.createVNode(_component_phabricator_tasks, {
      tasks: _ctx.wish.tasks,
      "onUpdate:tasks": [
        _cache[13] || (_cache[13] = $event => ((_ctx.wish.tasks) = $event)),
        _cache[14] || (_cache[14] = $event => (_ctx.updateField( 'tasks', $event )))
      ],
      disabled: _ctx.formDisabled
    }, null, 8 /* PROPS */, ["tasks", "disabled"]),
    vue.createElementVNode("section", _hoisted_1$1, [
      _hoisted_2,
      vue.createCommentVNode(" eslint-disable-next-line vue/no-v-html "),
      vue.createElementVNode("p", {
        innerHTML: _ctx.$i18n( 'wikimedia-copyrightwarning' ).parse()
      }, null, 8 /* PROPS */, _hoisted_3),
      vue.createVNode(_component_cdx_button, {
        weight: "primary",
        action: "progressive",
        disabled: _ctx.formDisabled,
        class: "wishlist-intake-submit",
        onClick: _ctx.handleSubmit
      }, {
        default: vue.withCtx(() => [
          (_ctx.exists)
            ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_4, vue.toDisplayString(_ctx.$i18n( 'communitywishlist-save' ).text()), 1 /* TEXT */))
            : (vue.openBlock(), vue.createElementBlock("span", _hoisted_5, vue.toDisplayString(_ctx.$i18n( 'communitywishlist-publish' ).text()), 1 /* TEXT */))
        ]),
        _: 1 /* STABLE */
      }, 8 /* PROPS */, ["disabled", "onClick"]),
      vue.createElementVNode("a", {
        href: _ctx.returnto,
        class: "cdx-button cdx-button--fake-button--enabled cdx-button--weight-quiet wishlist-intake-cancel"
      }, vue.toDisplayString(_ctx.$i18n( 'cancel' ).text()), 9 /* TEXT, PROPS */, _hoisted_6),
      (_ctx.formError)
        ? (vue.openBlock(), vue.createBlock(_component_cdx_message, {
            key: 0,
            type: "error",
            class: "wishlist-intake-form-error"
          }, {
            default: vue.withCtx(() => [
              vue.createCommentVNode(" eslint-disable-next-line vue/no-v-html "),
              vue.createElementVNode("p", null, [
                vue.createElementVNode("strong", { innerHTML: _ctx.formErrorMsg }, null, 8 /* PROPS */, _hoisted_7)
              ]),
              vue.createCommentVNode(" eslint-disable-next-line vue/no-v-html "),
              vue.createElementVNode("div", { innerHTML: _ctx.formError }, null, 8 /* PROPS */, _hoisted_8)
            ]),
            _: 1 /* STABLE */
          }))
        : vue.createCommentVNode("v-if", true)
    ])
  ], 32 /* NEED_HYDRATION */))
}

__$styleInject(".wishlist-intake-container .cdx-field,\n.wishlist-intake-container section:last-child,\n.wishlist-intake-container .wishlist-intake-form-error {\n  margin-top: 32px;\n}\n.wishlist-intake-form-footer .cdx-checkbox {\n  margin: 16px auto;\n}\n.wishlist-intake-cancel {\n  margin-left: 12px;\n}\n[dir='rtl'] .wishlist-intake-cancel {\n  margin-left: 0;\n  margin-right: 12px;\n}\n");

script$1.render = render$1;

/**
 * This class loads and saves the focus area voting subpages.
 */
class FocusAreaPage {

	constructor() {
		this.api = new mw.Api();
		this.votesPageName = this.getBasePageName() + '/Votes';
		this.voteCountPageName = this.getBasePageName() + config.focusAreaVoteCountSuffix;
	}

	/**
	 * Get the name of the Translate 'source' page for this page.
	 *
	 * @return {string}
	 */
	getBasePageName() {
		let pageName = mw.config.get( 'wgPageName' );
		if ( mw.config.get( 'wgTranslatePageTranslation' ) === 'translation' ) {
			pageName = pageName.slice( 0, pageName.length - mw.config.get( 'wgPageContentLanguage' ).length - 1 );
		}
		return pageName;
	}

	/**
	 * Load the current page's votes data.
	 *
	 * @return {Promise<string>}
	 */
	loadVotes() {
		return this.api.get( {
			action: 'query',
			titles: this.votesPageName,
			prop: 'revisions',
			rvprop: [ 'content', 'timestamp' ],
			rvslots: 'main',
			curtimestamp: true,
			assert: 'user',
			format: 'json',
			formatversion: 2
		} ).then( ( response ) => {
			const page = response.query && response.query.pages ? response.query.pages[ 0 ] : {};
			if ( page.missing ) {
				return '';
			}
			this.starttimestamp = response.curtimestamp;
			this.basetimestamp = page.revisions[ 0 ].timestamp;
			return page.revisions[ 0 ].slots.main.content;
		} );
	}

	/**
	 * Add a vote template to the votes list, and save the full wikitext
	 * for both the Votes page and the Vote_count page.
	 *
	 * @param {string} votes The votes wikitext.
	 * @param {string} comment The current user's support comment.
	 * @return {Promise}
	 */
	addVote( votes, comment ) {
		if ( this.alreadyVoted( votes ) ) {
			// @todo If already voted, change the timestamp and comment of the existing vote.
			return Promise.resolve();
		}
		// @todo Construct support template wikitext somewhere else.
		const newVote = '{{' + config.supportTemplate +
			' |username=' + mw.config.get( 'wgUserName' ) +
			' |timestamp=' + ( new Date() ).toISOString() +
			' |comment=' + comment.replace( '|', '{{!}}' ) +
			' }}';
		votes = votes.trim() + '\n' + newVote;
		// Count the votes by counting the Support template occurences.
		const escapedTemplateName = mw.util.escapeRegExp( config.supportTemplate );
		const voteCount = votes.match( new RegExp( '\\{\\{' + escapedTemplateName, 'g' ) ).length;
		// Save the votes page.
		return this.api.postWithEditToken( this.api.assertCurrentUser( {
			action: 'edit',
			title: this.votesPageName,
			text: votes,
			formatversion: 2,
			// Tag as having been edited by the gadget.
			tags: [ 'community-wishlist' ],
			// Protect against conflicts
			basetimestamp: this.basetimestamp,
			starttimestamp: this.curtimestamp,
			// Localize errors
			uselang: mw.config.get( 'wgUserLanguage' ),
			errorformat: 'html',
			errorlang: mw.config.get( 'wgUserLanguage' ),
			errorsuselocal: true
		} ) ).then( () => {
			// Update vote count after saving the vote.
			return this.api.postWithEditToken( this.api.assertCurrentUser( {
				action: 'edit',
				title: this.voteCountPageName,
				text: voteCount,
				formatversion: 2,
				starttimestamp: this.curtimestamp,
				// Tag as having been edited by the gadget.
				tags: [ 'community-wishlist' ],
				// Localize errors
				uselang: mw.config.get( 'wgUserLanguage' ),
				errorformat: 'html',
				errorlang: mw.config.get( 'wgUserLanguage' ),
				errorsuselocal: true
			} ) );
		} );
	}

	/**
	 * Check if the current user has already voted.
	 *
	 * @param {string} votesWikitext
	 * @return {boolean}
	 */
	alreadyVoted( votesWikitext ) {
		const escapedUsername = mw.util.escapeRegExp( mw.config.get( 'wgUserName' ) );
		const regex = new RegExp( 'username\\s*=\\s*' + escapedUsername );
		return votesWikitext.match( regex ) !== null;
	}
}

const focusAreaPage = new FocusAreaPage();

var script = vue.defineComponent( {
	name: 'FocusArea',
	components: {
		CdxButton: codex.CdxButton,
		CdxDialog: codex.CdxDialog,
		CdxField: codex.CdxField,
		CdxTextArea: codex.CdxTextArea,
		CdxMessage: codex.CdxMessage
	},
	data() {
		return {
			open: false,
			submitting: false,
			comment: '',
			hasVoted: false,
			showVotedMessage: false
		};
	},
	computed: {
		dialogTitle() {
			return mw.msg(
				'communitywishlist-support-focus-area-dialog-title',
				WebUtil.getFocusAreaSlug()
			);
		},
		primaryAction() {
			return {
				label: mw.msg( 'communitywishlist-support' ),
				actionType: 'progressive',
				disabled: this.submitting
			};
		},
		defaultAction() {
			return {
				label: mw.msg( 'cancel' ),
				disabled: this.submitting
			};
		}
	},
	methods: {
		updateComment( event ) {
			this.comment = event.target.value;
		},
		onPrimaryAction() {
			this.submitting = true;
			focusAreaPage.loadVotes().then( ( votes ) => {
				focusAreaPage.addVote( votes, this.comment ).then( ( editResult ) => {
					this.open = false;
					if ( !editResult ) {
						return;
					}
					if ( editResult.edit.result === 'Success' ) {
						// Purge and reload the page.
						const postArgs = { action: 'purge', titles: mw.config.get( 'wgPageName' ) };
						( new mw.Api() ).post( postArgs ).then( ( purgeRes ) => {
							mw.storage.session.set( config.voteRecordedStorageName, 1 );
							location.href = mw.util.getUrl( purgeRes.purge[ 0 ].title ) + '#voting';
							// Also reload, in case they were already at #voting.
							location.reload();
						} );
					}
				} );
			} );
		},
		removeVote() {
			// @todo Implement.
		}
	},
	beforeMount() {
		focusAreaPage.loadVotes().then( ( votes ) => {
			if ( votes && focusAreaPage.alreadyVoted( votes ) ) {
				this.hasVoted = true;
				// Also check for the session storage param for the post-voting message.
				this.showVotedMessage = mw.storage.session.get( config.voteRecordedStorageName ) !== null;
				mw.storage.session.remove( config.voteRecordedStorageName );
			}
		} );
	}
} );

const _withScopeId = n => (vue.pushScopeId("data-v-67acc686"),n=n(),vue.popScopeId(),n);
const _hoisted_1 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/vue.createElementVNode("span", { class: "cdx-button__icon cdx-demo-css-icon--check" }, null, -1 /* HOISTED */));

function render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_cdx_button = vue.resolveComponent("cdx-button");
  const _component_cdx_message = vue.resolveComponent("cdx-message");
  const _component_cdx_text_area = vue.resolveComponent("cdx-text-area");
  const _component_cdx_field = vue.resolveComponent("cdx-field");
  const _component_cdx_dialog = vue.resolveComponent("cdx-dialog");

  return (vue.openBlock(), vue.createElementBlock(vue.Fragment, null, [
    (!_ctx.hasVoted)
      ? (vue.openBlock(), vue.createBlock(_component_cdx_button, {
          key: 0,
          action: "progressive",
          weight: "primary",
          onClick: _cache[0] || (_cache[0] = $event => (_ctx.open = true))
        }, {
          default: vue.withCtx(() => [
            vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-support-focus-area' ).text()), 1 /* TEXT */)
          ]),
          _: 1 /* STABLE */
        }))
      : (vue.openBlock(), vue.createBlock(_component_cdx_button, {
          key: 1,
          onClick: _ctx.removeVote
        }, {
          default: vue.withCtx(() => [
            _hoisted_1,
            vue.createTextVNode(" " + vue.toDisplayString(_ctx.$i18n( 'communitywishlist-supported' ).text()), 1 /* TEXT */)
          ]),
          _: 1 /* STABLE */
        }, 8 /* PROPS */, ["onClick"])),
    (_ctx.showVotedMessage)
      ? (vue.openBlock(), vue.createBlock(_component_cdx_message, {
          key: 2,
          type: "success",
          "allow-user-dismiss": ""
        }, {
          default: vue.withCtx(() => [
            vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-support-focus-area-confirmed' ).text()), 1 /* TEXT */)
          ]),
          _: 1 /* STABLE */
        }))
      : vue.createCommentVNode("v-if", true),
    vue.createVNode(_component_cdx_dialog, {
      open: _ctx.open,
      "onUpdate:open": _cache[1] || (_cache[1] = $event => ((_ctx.open) = $event)),
      title: _ctx.dialogTitle,
      "use-close-button": true,
      "primary-action": _ctx.primaryAction,
      "default-action": _ctx.defaultAction,
      onPrimary: _ctx.onPrimaryAction,
      onDefault: _cache[2] || (_cache[2] = $event => (_ctx.open = false))
    }, {
      default: vue.withCtx(() => [
        vue.createVNode(_component_cdx_field, null, {
          label: vue.withCtx(() => [
            vue.createTextVNode(vue.toDisplayString(_ctx.$i18n( 'communitywishlist-optional-comment' ).text()), 1 /* TEXT */)
          ]),
          default: vue.withCtx(() => [
            vue.createVNode(_component_cdx_text_area, {
              disabled: _ctx.submitting,
              onInput: _ctx.updateComment
            }, null, 8 /* PROPS */, ["disabled", "onInput"])
          ]),
          _: 1 /* STABLE */
        })
      ]),
      _: 1 /* STABLE */
    }, 8 /* PROPS */, ["open", "title", "primary-action", "default-action", "onPrimary"])
  ], 64 /* STABLE_FRAGMENT */))
}

__$styleInject(".cdx-button[data-v-67acc686],\n.cdx-message[data-v-67acc686] {\n  margin-bottom: 16px;\n}\n/* Hack in check icon. */\n/* FIXME: Use actual Codex component when we move to the extension. */\n.cdx-demo-css-icon--check[data-v-67acc686] {\n  min-width: 20px;\n  min-height: 20px;\n  width: 1.25em;\n  height: 1.25em;\n  display: inline-block;\n  /* stylelint-disable-next-line declaration-no-important */\n  background-color: transparent !important;\n  background-image: url(https://upload.wikimedia.org/wikipedia/commons/f/f4/Codex_icon_check.svg );\n}\n");

script.render = render;
script.__scopeId = "data-v-67acc686";

const api = new mw.Api();

/**
 * If the page already exists, pre-fetch the content
 * so that it's available when the form is loaded.
 *
 * @return {Promise|jQuery.Promise}
 */
function loadWishData() {
	if ( WebUtil.isNewWish() ) {
		return Promise.resolve();
	}

	return api.get( {
		action: 'query',
		format: 'json',
		prop: 'revisions',
		titles: mw.config.get( 'wgPageName' ),
		rvprop: [ 'content', 'timestamp' ],
		rvslots: 'main',
		formatversion: 2,
		assert: 'user',
		curtimestamp: true
	} ).then( ( res ) => {
		const page = res.query && res.query.pages ? res.query.pages[ 0 ] : {};
		if ( page.missing ) {
			// TODO: show button to create wish and pre-fill the title with the subpage name.
			return {};
		}
		const revision = page.revisions[ 0 ];
		const template = Util.getWishTemplate();
		const wikitext = revision.slots.main.content;
		const wish = template.getWish( wikitext, mw.config.get( 'wgPageName' ) );
		// Confirm that we can parse the wikitext.
		if ( !wish ) {
			return null;
		}
		const wishData = {
			status: wish.status,
			type: wish.type,
			title: wish.title,
			description: wish.description,
			audience: wish.audience,
			tasks: Wish.getArrayFromValue( wish.tasks ),
			proposer: wish.proposer,
			created: wish.created,
			projects: Wish.getArrayFromValue( wish.projects ),
			otherproject: wish.otherproject,
			area: wish.area,
			baselang: wish.baselang
		};
		// Confirm that we can parse and then re-create the same wikitext.
		if ( template.getWikitext( wishData ) !== wikitext ) {
			WebUtil.logError( 'Parsing failed for ', mw.config.get( 'wgPageName' ) );
		}
		return Object.assign( {
			// For edit conflict detection.
			basetimestamp: revision.timestamp,
			curtimestamp: res.curtimestamp
		}, wishData );
	} );
}

/**
 * Show a banner after a wish has been saved.
 */
function showPostEditBanner() {
	// Close image.
	const closeImg = document.createElement( 'img' );
	closeImg.src = 'https://upload.wikimedia.org/wikipedia/commons/8/82/Codex_icon_close.svg';
	closeImg.alt = mw.msg( 'communitywishlist-close' );
	// Close button.
	const closeButton = document.createElement( 'button' );
	closeButton.className = 'cdx-button cdx-button--action-default cdx-button--weight-quiet cdx-button--size-medium cdx-button--icon-only cdx-message__dismiss-button';
	closeButton.ariaLabel = mw.msg( 'communitywishlist-close' );
	// Message icon.
	const messageIcon = document.createElement( 'span' );
	messageIcon.className = 'cdx-message__icon';
	// View wishes link.
	const viewWishesLink = document.createElement( 'a' );
	viewWishesLink.href = mw.util.getUrl( 'Special:MyLanguage/Community Wishlist/Wishes' );
	viewWishesLink.textContent = mw.msg( 'communitywishlist-view-all-wishes' );
	// Message content.
	const messageContent = document.createElement( 'div' );
	messageContent.className = 'cdx-message__content';
	const messageContentMsg = mw.config.get( 'wgPostEdit' ) === 'created' ? 'communitywishlist-create-success' : 'communitywishlist-edit-success';
	// Messages that can be used here:
	// * communitywishlist-create-success
	// * communitywishlist-edit-success
	messageContent.textContent = mw.msg( messageContentMsg ) + ' ';
	// Message container.
	const messageContainer = document.createElement( 'div' );
	messageContainer.className = 'cdx-message cdx-message--block cdx-message--success';
	messageContainer.ariaLive = 'polite';
	// Append elements.
	closeButton.appendChild( closeImg );
	messageContent.appendChild( viewWishesLink );
	messageContainer.appendChild( messageIcon );
	messageContainer.appendChild( messageContent );
	messageContainer.appendChild( closeButton );
	document.querySelector( '.mw-body-content' ).prepend( messageContainer );
	// Close the banner when the close button is clicked.
	closeButton.addEventListener( 'click', () => messageContainer.remove() );
}

/**
 * Show an error message when a wish fails to load.
 *
 * @param {HTMLElement} mwContentText
 */
function handleWishLoadError( mwContentText ) {
	const errorMsg = mw.message( 'communityrequests-wish-loading-error',
		window.location.href,
		`Special:EditPage/${ mw.config.get( 'wgPageName' ) }`,
		'Talk:Community Wishlist'
	);
	mwContentText.prepend(
		WebUtil.getMessageBox( errorMsg, 'error' )
	);
}

/**
 * Entry point for the gadget.
 */
$( () => {
	// In case Category:Community_Wishlist/Intake is on an unexpected page.
	if ( !( WebUtil.isWishRelatedPage() || WebUtil.isFocusAreaPage() ) ) {
		return;
	}

	// Don't load the form on protected pages (T369352).
	// We don't have config.importedMessages yet, so instead of showing an error
	// we'll redirect to action=edit where they can review the source.
	if ( !mw.config.get( 'wgIsProbablyEditable' ) && WebUtil.isWishEdit() ) {
		window.location.replace( mw.util.getUrl( WebUtil.getPageName(), { action: 'edit' } ) );
		return;
	}

	const mwContentText = document.querySelector( '#mw-content-text' );

	// Load all required i18n messages.
	const promises = [
		api.loadMessages( config.importedMessages ),
		WebUtil.setOnWikiMessages( api )
	];

	if ( WebUtil.isWishEdit() ) {
		// Pre-fetch the wish data.
		promises.push( loadWishData() );
	}

	// Load all required i18n messages, and then the rest of the gadget.
	Promise.all( promises ).then( ( res ) => {
		const Vue = require( 'vue' );

		// Load the focus area app if we're on a focus area page.
		if ( WebUtil.isFocusAreaPage() ) {
			const focusAreaApp = document.createElement( 'div' );
			document.querySelector( '.community-wishlist-voting-btn' )
				.closest( 'p' )
				.replaceWith( focusAreaApp );
			Vue.createMwApp( script ).mount( focusAreaApp );
			return;
		}

		mw.hook( 'postEdit' ).add( showPostEditBanner );

		if ( !WebUtil.shouldShowForm() ) {
			return;
		}

		if ( res[ 0 ] === null ) {
			handleWishLoadError( mwContentText );
			return;
		}
		let wishData = {};
		if ( res[ 2 ] && res[ 2 ].title ) {
			wishData = res[ 2 ];
		}

		const root = document.createElement( 'div' );
		root.className = 'wishlist-intake-container';
		mwContentText.replaceWith( root );
		Vue.createMwApp( script$1, wishData ).mount( root );
	} );

} );

} );
// </nowiki>