<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.recessim.com/w/index.php?action=history&amp;feed=atom&amp;title=MediaWiki%3AGadget-VEBulkUpload.js</id>
	<title>MediaWiki:Gadget-VEBulkUpload.js - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.recessim.com/w/index.php?action=history&amp;feed=atom&amp;title=MediaWiki%3AGadget-VEBulkUpload.js"/>
	<link rel="alternate" type="text/html" href="https://wiki.recessim.com/w/index.php?title=MediaWiki:Gadget-VEBulkUpload.js&amp;action=history"/>
	<updated>2026-09-02T20:31:41Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.43.9</generator>
	<entry>
		<id>https://wiki.recessim.com/w/index.php?title=MediaWiki:Gadget-VEBulkUpload.js&amp;diff=3657&amp;oldid=prev</id>
		<title>Hash: Use mediawiki.widgets.CategoryMultiselectWidget (CategorySelector does not exist in MW 1.43); base the tool on ve.ui.FragmentWindowTool</title>
		<link rel="alternate" type="text/html" href="https://wiki.recessim.com/w/index.php?title=MediaWiki:Gadget-VEBulkUpload.js&amp;diff=3657&amp;oldid=prev"/>
		<updated>2026-08-22T19:26:42Z</updated>

		<summary type="html">&lt;p&gt;Use mediawiki.widgets.CategoryMultiselectWidget (CategorySelector does not exist in MW 1.43); base the tool on ve.ui.FragmentWindowTool&lt;/p&gt;
&lt;table style=&quot;background-color: #fff; color: #202122;&quot; data-mw=&quot;interface&quot;&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;tr class=&quot;diff-title&quot; lang=&quot;en&quot;&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;← Older revision&lt;/td&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;Revision as of 19:26, 22 August 2026&lt;/td&gt;
				&lt;/tr&gt;&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l20&quot;&gt;Line 20:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 20:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;	&amp;#039;use strict&amp;#039;;&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;	&amp;#039;use strict&amp;#039;;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;	if ( typeof ve === &#039;undefined&#039; || !ve.ui || !ve.ui.FragmentDialog ) {&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;	if ( typeof ve === &#039;undefined&#039; || !ve.ui || !ve.ui.FragmentDialog &lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;|| !ve.ui.FragmentWindowTool &lt;/ins&gt;) {&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		// Not inside VisualEditor — nothing to do.&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		// Not inside VisualEditor — nothing to do.&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		return;&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		return;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l344&quot;&gt;Line 344:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 344:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;			placeholder: &amp;#039;Shared description saved on every file page (optional)&amp;#039;&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;			placeholder: &amp;#039;Shared description saved on every file page (optional)&amp;#039;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		} );&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		} );&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		this.categoriesInput = new mw.widgets.&lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;CategorySelector&lt;/del&gt;();&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		this.categoriesInput = new mw.widgets.&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;CategoryMultiselectWidget&lt;/ins&gt;();&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		this.galleryField = new OO.ui.FieldLayout( this.heightsInput, {&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		this.galleryField = new OO.ui.FieldLayout( this.heightsInput, {&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l408&quot;&gt;Line 408:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 408:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;				this.statusLabel.setLabel( &amp;#039;&amp;#039; );&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;				this.statusLabel.setLabel( &amp;#039;&amp;#039; );&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;				this.descriptionInput.setValue( &amp;#039;&amp;#039; );&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;				this.descriptionInput.setValue( &amp;#039;&amp;#039; );&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;				this.categoriesInput.&lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;setItemsFromData&lt;/del&gt;( [] );&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;				this.categoriesInput.&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;setValue&lt;/ins&gt;( [] );&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;				this.insertAsInput.setValue( &amp;#039;gallery&amp;#039; );&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;				this.insertAsInput.setValue( &amp;#039;gallery&amp;#039; );&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;				this.captionsInput.setSelected( true );&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;				this.captionsInput.setSelected( true );&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l733&quot;&gt;Line 733:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 733:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		var parts = [],&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		var parts = [],&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;			description = this.descriptionInput.getValue().trim(),&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;			description = this.descriptionInput.getValue().trim(),&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;			categories = this.categoriesInput.&lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;getItemsData&lt;/del&gt;();&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;			categories = this.categoriesInput.&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;getValue&lt;/ins&gt;();&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		if ( description ) {&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		if ( description ) {&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l906&quot;&gt;Line 906:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 906:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		BulkUploadTool.super.apply( this, arguments );&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;		BulkUploadTool.super.apply( this, arguments );&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;	}&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;	}&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;	OO.inheritClass( BulkUploadTool, ve.ui.&lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Tool &lt;/del&gt;);&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;	OO.inheritClass( BulkUploadTool, ve.ui.&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;FragmentWindowTool &lt;/ins&gt;);&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;	BulkUploadTool.static.name = &amp;#039;bulkUpload&amp;#039;;&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;	BulkUploadTool.static.name = &amp;#039;bulkUpload&amp;#039;;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;	BulkUploadTool.static.group = &amp;#039;object&amp;#039;;&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;	BulkUploadTool.static.group = &amp;#039;object&amp;#039;;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;

&lt;!-- diff cache key RECESSIM:diff:1.41:old-3651:rev-3657:php=table --&gt;
&lt;/table&gt;</summary>
		<author><name>Hash</name></author>
	</entry>
	<entry>
		<id>https://wiki.recessim.com/w/index.php?title=MediaWiki:Gadget-VEBulkUpload.js&amp;diff=3651&amp;oldid=prev</id>
		<title>Hash: Bulk file upload for VisualEditor (gadget)</title>
		<link rel="alternate" type="text/html" href="https://wiki.recessim.com/w/index.php?title=MediaWiki:Gadget-VEBulkUpload.js&amp;diff=3651&amp;oldid=prev"/>
		<updated>2026-08-22T19:21:56Z</updated>

		<summary type="html">&lt;p&gt;Bulk file upload for VisualEditor (gadget)&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;/**&lt;br /&gt;
 * VEBulkUpload — bulk file upload for VisualEditor.&lt;br /&gt;
 *&lt;br /&gt;
 * Drag a group of files anywhere onto the page you are visually editing (or use&lt;br /&gt;
 * Insert → &amp;quot;Upload files&amp;quot;), give them a shared description/categories, and they&lt;br /&gt;
 * are uploaded and inserted as a &amp;lt;gallery&amp;gt;, as thumbnails, or as a link list.&lt;br /&gt;
 *&lt;br /&gt;
 * Single-file drops are deliberately NOT intercepted — those keep going to&lt;br /&gt;
 * VisualEditor&amp;#039;s own media dialog (Extension:EnhancedUpload).&lt;br /&gt;
 *&lt;br /&gt;
 * Loaded into VisualEditor through $wgVisualEditorPluginModules, so this file&lt;br /&gt;
 * executes before the editor toolbar is built.&lt;br /&gt;
 *&lt;br /&gt;
 * Documentation: [[Help:Uploading files]]&lt;br /&gt;
 * Maintainer note: only three VisualEditor APIs are used — ve.ui.toolFactory,&lt;br /&gt;
 * ve.ui.dataTransferHandlerFactory/DataTransferItem, and the ce.Surface drop&lt;br /&gt;
 * events. Re-check those after a MediaWiki upgrade.&lt;br /&gt;
 */&lt;br /&gt;
( function () {&lt;br /&gt;
	&amp;#039;use strict&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
	if ( typeof ve === &amp;#039;undefined&amp;#039; || !ve.ui || !ve.ui.FragmentDialog ) {&lt;br /&gt;
		// Not inside VisualEditor — nothing to do.&lt;br /&gt;
		return;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	var IMAGE_EXTENSIONS = [ &amp;#039;jpg&amp;#039;, &amp;#039;jpeg&amp;#039;, &amp;#039;png&amp;#039;, &amp;#039;gif&amp;#039;, &amp;#039;svg&amp;#039;, &amp;#039;webp&amp;#039;, &amp;#039;tif&amp;#039;, &amp;#039;tiff&amp;#039; ],&lt;br /&gt;
		TITLE_QUERY_CHUNK = 45,&lt;br /&gt;
		api = null,&lt;br /&gt;
		allowedExtensionsPromise = null;&lt;br /&gt;
&lt;br /&gt;
	function getApi() {&lt;br /&gt;
		if ( !api ) {&lt;br /&gt;
			api = new mw.Api();&lt;br /&gt;
		}&lt;br /&gt;
		return api;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function getExtension( name ) {&lt;br /&gt;
		var parts = String( name ).split( &amp;#039;.&amp;#039; );&lt;br /&gt;
		return parts.length &amp;gt; 1 ? parts.pop().toLowerCase() : &amp;#039;&amp;#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function isImageName( name ) {&lt;br /&gt;
		return IMAGE_EXTENSIONS.indexOf( getExtension( name ) ) !== -1;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function stripExtension( name ) {&lt;br /&gt;
		return String( name ).replace( /\.[^.]+$/, &amp;#039;&amp;#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function captionFor( name ) {&lt;br /&gt;
		return stripExtension( name ).replace( /[_-]+/g, &amp;#039; &amp;#039; ).replace( /\s+/g, &amp;#039; &amp;#039; ).trim();&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	/**&lt;br /&gt;
	 * Turn a local filename into a valid File: page name.&lt;br /&gt;
	 *&lt;br /&gt;
	 * @param {string} name&lt;br /&gt;
	 * @return {string}&lt;br /&gt;
	 */&lt;br /&gt;
	function normaliseFileName( name ) {&lt;br /&gt;
		var title = mw.Title.newFromFileName( name );&lt;br /&gt;
		return title ? title.getMainText() : String( name ).replace( /[#&amp;lt;&amp;gt;[\]|{}]/g, &amp;#039;&amp;#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	/**&lt;br /&gt;
	 * Extensions this wiki accepts, from siteinfo. Cached for the session.&lt;br /&gt;
	 * Fails open (empty list = accept everything) so a flaky API can&amp;#039;t block uploads.&lt;br /&gt;
	 *&lt;br /&gt;
	 * @return {jQuery.Promise} Promise resolving with an array of lowercase extensions&lt;br /&gt;
	 */&lt;br /&gt;
	function getAllowedExtensions() {&lt;br /&gt;
		if ( !allowedExtensionsPromise ) {&lt;br /&gt;
			allowedExtensionsPromise = getApi().get( {&lt;br /&gt;
				action: &amp;#039;query&amp;#039;,&lt;br /&gt;
				meta: &amp;#039;siteinfo&amp;#039;,&lt;br /&gt;
				siprop: &amp;#039;fileextensions&amp;#039;,&lt;br /&gt;
				formatversion: 2&lt;br /&gt;
			} ).then( function ( data ) {&lt;br /&gt;
				return ( data.query.fileextensions || [] ).map( function ( e ) {&lt;br /&gt;
					return String( e.ext ).toLowerCase();&lt;br /&gt;
				} );&lt;br /&gt;
			}, function () {&lt;br /&gt;
				return [];&lt;br /&gt;
			} );&lt;br /&gt;
		}&lt;br /&gt;
		return allowedExtensionsPromise;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function apiErrorMessage( code, result ) {&lt;br /&gt;
		if ( result &amp;amp;&amp;amp; result.errors &amp;amp;&amp;amp; result.errors[ 0 ] ) {&lt;br /&gt;
			return result.errors[ 0 ].html || result.errors[ 0 ].text || code;&lt;br /&gt;
		}&lt;br /&gt;
		if ( result &amp;amp;&amp;amp; result.error &amp;amp;&amp;amp; result.error.info ) {&lt;br /&gt;
			return result.error.info;&lt;br /&gt;
		}&lt;br /&gt;
		return String( code || &amp;#039;unknown error&amp;#039; );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	/* ---------------------------------------------------------------- rows -- */&lt;br /&gt;
&lt;br /&gt;
	/**&lt;br /&gt;
	 * One queued file in the dialog.&lt;br /&gt;
	 *&lt;br /&gt;
	 * @class&lt;br /&gt;
	 * @param {File} file&lt;br /&gt;
	 * @param {Object} [config]&lt;br /&gt;
	 */&lt;br /&gt;
	function FileRow( file, config ) {&lt;br /&gt;
		var row = this;&lt;br /&gt;
&lt;br /&gt;
		FileRow.super.call( this, config || {} );&lt;br /&gt;
&lt;br /&gt;
		this.file = file;&lt;br /&gt;
		this.exists = false;&lt;br /&gt;
		this.done = false;&lt;br /&gt;
		this.uploadedName = null;&lt;br /&gt;
		this.error = null;&lt;br /&gt;
&lt;br /&gt;
		this.nameInput = new OO.ui.TextInputWidget( {&lt;br /&gt;
			value: normaliseFileName( file.name ),&lt;br /&gt;
			classes: [ &amp;#039;ve-bulkupload-row-name&amp;#039; ]&lt;br /&gt;
		} );&lt;br /&gt;
		this.nameInput.connect( this, { change: &amp;#039;onNameChange&amp;#039; } );&lt;br /&gt;
&lt;br /&gt;
		this.policyInput = new OO.ui.DropdownInputWidget( {&lt;br /&gt;
			options: [&lt;br /&gt;
				{ data: &amp;#039;rename&amp;#039;, label: &amp;#039;Upload under a new name&amp;#039; },&lt;br /&gt;
				{ data: &amp;#039;existing&amp;#039;, label: &amp;#039;Use the file already on the wiki&amp;#039; },&lt;br /&gt;
				{ data: &amp;#039;overwrite&amp;#039;, label: &amp;#039;Overwrite the existing file&amp;#039; },&lt;br /&gt;
				{ data: &amp;#039;skip&amp;#039;, label: &amp;#039;Skip this file&amp;#039; }&lt;br /&gt;
			],&lt;br /&gt;
			value: &amp;#039;rename&amp;#039;,&lt;br /&gt;
			classes: [ &amp;#039;ve-bulkupload-row-policy&amp;#039; ]&lt;br /&gt;
		} );&lt;br /&gt;
		this.policyInput.toggle( false );&lt;br /&gt;
&lt;br /&gt;
		this.statusLabel = new OO.ui.LabelWidget( {&lt;br /&gt;
			label: &amp;#039;&amp;#039;,&lt;br /&gt;
			classes: [ &amp;#039;ve-bulkupload-row-status&amp;#039; ]&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		this.removeButton = new OO.ui.ButtonWidget( {&lt;br /&gt;
			icon: &amp;#039;trash&amp;#039;,&lt;br /&gt;
			framed: false,&lt;br /&gt;
			title: &amp;#039;Remove from this upload&amp;#039;,&lt;br /&gt;
			classes: [ &amp;#039;ve-bulkupload-row-remove&amp;#039; ]&lt;br /&gt;
		} );&lt;br /&gt;
		this.removeButton.connect( this, { click: &amp;#039;onRemove&amp;#039; } );&lt;br /&gt;
&lt;br /&gt;
		this.$thumb = $( &amp;#039;&amp;lt;div&amp;gt;&amp;#039; ).addClass( &amp;#039;ve-bulkupload-row-thumb&amp;#039; );&lt;br /&gt;
		if ( isImageName( file.name ) &amp;amp;&amp;amp; window.URL &amp;amp;&amp;amp; URL.createObjectURL ) {&lt;br /&gt;
			this.objectUrl = URL.createObjectURL( file );&lt;br /&gt;
			this.$thumb.append( $( &amp;#039;&amp;lt;img&amp;gt;&amp;#039; ).attr( &amp;#039;src&amp;#039;, this.objectUrl ).on( &amp;#039;error&amp;#039;, function () {&lt;br /&gt;
				row.$thumb.empty().text( getExtension( file.name ).toUpperCase() );&lt;br /&gt;
			} ) );&lt;br /&gt;
		} else {&lt;br /&gt;
			this.$thumb.addClass( &amp;#039;ve-bulkupload-row-thumb-generic&amp;#039; )&lt;br /&gt;
				.text( getExtension( file.name ).toUpperCase() || &amp;#039;?&amp;#039; );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		this.progressBar = new OO.ui.ProgressBarWidget( { progress: 0 } );&lt;br /&gt;
		this.progressBar.toggle( false );&lt;br /&gt;
&lt;br /&gt;
		this.$element&lt;br /&gt;
			.addClass( &amp;#039;ve-bulkupload-row&amp;#039; )&lt;br /&gt;
			.append(&lt;br /&gt;
				this.$thumb,&lt;br /&gt;
				$( &amp;#039;&amp;lt;div&amp;gt;&amp;#039; ).addClass( &amp;#039;ve-bulkupload-row-body&amp;#039; ).append(&lt;br /&gt;
					this.nameInput.$element,&lt;br /&gt;
					$( &amp;#039;&amp;lt;div&amp;gt;&amp;#039; ).addClass( &amp;#039;ve-bulkupload-row-meta&amp;#039; ).append(&lt;br /&gt;
						$( &amp;#039;&amp;lt;span&amp;gt;&amp;#039; ).addClass( &amp;#039;ve-bulkupload-row-size&amp;#039; )&lt;br /&gt;
							.text( formatSize( file.size ) ),&lt;br /&gt;
						this.statusLabel.$element,&lt;br /&gt;
						this.policyInput.$element&lt;br /&gt;
					),&lt;br /&gt;
					this.progressBar.$element&lt;br /&gt;
				),&lt;br /&gt;
				this.removeButton.$element&lt;br /&gt;
			);&lt;br /&gt;
	}&lt;br /&gt;
	OO.inheritClass( FileRow, OO.ui.Widget );&lt;br /&gt;
&lt;br /&gt;
	function formatSize( bytes ) {&lt;br /&gt;
		if ( bytes &amp;gt; 1048576 ) {&lt;br /&gt;
			return ( bytes / 1048576 ).toFixed( 1 ) + &amp;#039; MB&amp;#039;;&lt;br /&gt;
		}&lt;br /&gt;
		return Math.max( 1, Math.round( bytes / 1024 ) ) + &amp;#039; KB&amp;#039;;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	FileRow.prototype.onRemove = function () {&lt;br /&gt;
		this.emit( &amp;#039;remove&amp;#039;, this );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	FileRow.prototype.onNameChange = function () {&lt;br /&gt;
		this.emit( &amp;#039;nameChange&amp;#039;, this );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	FileRow.prototype.getFileName = function () {&lt;br /&gt;
		return normaliseFileName( this.nameInput.getValue().trim() );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	FileRow.prototype.getPolicy = function () {&lt;br /&gt;
		return this.exists ? this.policyInput.getValue() : &amp;#039;rename&amp;#039;;&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	FileRow.prototype.setExists = function ( exists ) {&lt;br /&gt;
		this.exists = !!exists;&lt;br /&gt;
		this.policyInput.toggle( this.exists );&lt;br /&gt;
		this.setStatus( this.exists ? &amp;#039;A file with this name already exists.&amp;#039; : &amp;#039;&amp;#039;, this.exists ? &amp;#039;warning&amp;#039; : null );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	FileRow.prototype.setStatus = function ( text, kind ) {&lt;br /&gt;
		this.statusLabel.setLabel( text || &amp;#039;&amp;#039; );&lt;br /&gt;
		this.statusLabel.$element&lt;br /&gt;
			.toggleClass( &amp;#039;ve-bulkupload-status-warning&amp;#039;, kind === &amp;#039;warning&amp;#039; )&lt;br /&gt;
			.toggleClass( &amp;#039;ve-bulkupload-status-error&amp;#039;, kind === &amp;#039;error&amp;#039; )&lt;br /&gt;
			.toggleClass( &amp;#039;ve-bulkupload-status-done&amp;#039;, kind === &amp;#039;done&amp;#039; );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	FileRow.prototype.setProgress = function ( fraction ) {&lt;br /&gt;
		this.progressBar.toggle( true );&lt;br /&gt;
		this.progressBar.setProgress( Math.round( fraction * 100 ) );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	FileRow.prototype.setBusy = function ( busy ) {&lt;br /&gt;
		this.nameInput.setDisabled( busy );&lt;br /&gt;
		this.policyInput.setDisabled( busy );&lt;br /&gt;
		this.removeButton.setDisabled( busy );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	FileRow.prototype.destroy = function () {&lt;br /&gt;
		if ( this.objectUrl ) {&lt;br /&gt;
			URL.revokeObjectURL( this.objectUrl );&lt;br /&gt;
			this.objectUrl = null;&lt;br /&gt;
		}&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	/* -------------------------------------------------------------- dialog -- */&lt;br /&gt;
&lt;br /&gt;
	/**&lt;br /&gt;
	 * @class&lt;br /&gt;
	 * @param {Object} [config]&lt;br /&gt;
	 */&lt;br /&gt;
	function BulkUploadDialog( config ) {&lt;br /&gt;
		BulkUploadDialog.super.call( this, config );&lt;br /&gt;
	}&lt;br /&gt;
	OO.inheritClass( BulkUploadDialog, ve.ui.FragmentDialog );&lt;br /&gt;
&lt;br /&gt;
	BulkUploadDialog.static.name = &amp;#039;veBulkUpload&amp;#039;;&lt;br /&gt;
	BulkUploadDialog.static.title = &amp;#039;Upload files&amp;#039;;&lt;br /&gt;
	BulkUploadDialog.static.size = &amp;#039;large&amp;#039;;&lt;br /&gt;
	BulkUploadDialog.static.actions = [&lt;br /&gt;
		{&lt;br /&gt;
			action: &amp;#039;upload&amp;#039;,&lt;br /&gt;
			label: &amp;#039;Upload and insert&amp;#039;,&lt;br /&gt;
			flags: [ &amp;#039;primary&amp;#039;, &amp;#039;progressive&amp;#039; ],&lt;br /&gt;
			modes: &amp;#039;edit&amp;#039;&lt;br /&gt;
		},&lt;br /&gt;
		{&lt;br /&gt;
			label: &amp;#039;Cancel&amp;#039;,&lt;br /&gt;
			flags: [ &amp;#039;safe&amp;#039;, &amp;#039;close&amp;#039; ],&lt;br /&gt;
			modes: &amp;#039;edit&amp;#039;&lt;br /&gt;
		},&lt;br /&gt;
		{&lt;br /&gt;
			action: &amp;#039;abort&amp;#039;,&lt;br /&gt;
			label: &amp;#039;Stop&amp;#039;,&lt;br /&gt;
			flags: [ &amp;#039;safe&amp;#039; ],&lt;br /&gt;
			modes: &amp;#039;uploading&amp;#039;&lt;br /&gt;
		},&lt;br /&gt;
		{&lt;br /&gt;
			action: &amp;#039;finish&amp;#039;,&lt;br /&gt;
			label: &amp;#039;Close&amp;#039;,&lt;br /&gt;
			flags: [ &amp;#039;primary&amp;#039;, &amp;#039;progressive&amp;#039; ],&lt;br /&gt;
			modes: &amp;#039;result&amp;#039;&lt;br /&gt;
		}&lt;br /&gt;
	];&lt;br /&gt;
&lt;br /&gt;
	BulkUploadDialog.prototype.initialize = function () {&lt;br /&gt;
		var dialog = this;&lt;br /&gt;
&lt;br /&gt;
		BulkUploadDialog.super.prototype.initialize.call( this );&lt;br /&gt;
&lt;br /&gt;
		this.panel = new OO.ui.PanelLayout( { padded: true, expanded: false } );&lt;br /&gt;
&lt;br /&gt;
		// Drop zone / file picker&lt;br /&gt;
		this.$fileInput = $( &amp;#039;&amp;lt;input&amp;gt;&amp;#039; )&lt;br /&gt;
			.attr( { type: &amp;#039;file&amp;#039;, multiple: &amp;#039;multiple&amp;#039; } )&lt;br /&gt;
			.addClass( &amp;#039;ve-bulkupload-fileinput&amp;#039; )&lt;br /&gt;
			.on( &amp;#039;change&amp;#039;, function () {&lt;br /&gt;
				dialog.addFiles( Array.prototype.slice.call( this.files ) );&lt;br /&gt;
				this.value = &amp;#039;&amp;#039;;&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
		this.$dropZone = $( &amp;#039;&amp;lt;div&amp;gt;&amp;#039; )&lt;br /&gt;
			.addClass( &amp;#039;ve-bulkupload-dropzone&amp;#039; )&lt;br /&gt;
			.append(&lt;br /&gt;
				$( &amp;#039;&amp;lt;p&amp;gt;&amp;#039; ).text( &amp;#039;Drop files here, or&amp;#039; ),&lt;br /&gt;
				new OO.ui.ButtonWidget( {&lt;br /&gt;
					label: &amp;#039;Choose files&amp;#039;,&lt;br /&gt;
					icon: &amp;#039;upload&amp;#039;,&lt;br /&gt;
					flags: [ &amp;#039;progressive&amp;#039; ]&lt;br /&gt;
				} ).on( &amp;#039;click&amp;#039;, function () {&lt;br /&gt;
					dialog.$fileInput.trigger( &amp;#039;click&amp;#039; );&lt;br /&gt;
				} ).$element,&lt;br /&gt;
				this.$fileInput&lt;br /&gt;
			)&lt;br /&gt;
			.on( &amp;#039;dragover dragenter&amp;#039;, function ( e ) {&lt;br /&gt;
				e.preventDefault();&lt;br /&gt;
				e.stopPropagation();&lt;br /&gt;
				$( this ).addClass( &amp;#039;ve-bulkupload-dropzone-active&amp;#039; );&lt;br /&gt;
			} )&lt;br /&gt;
			.on( &amp;#039;dragleave dragend&amp;#039;, function () {&lt;br /&gt;
				$( this ).removeClass( &amp;#039;ve-bulkupload-dropzone-active&amp;#039; );&lt;br /&gt;
			} )&lt;br /&gt;
			.on( &amp;#039;drop&amp;#039;, function ( e ) {&lt;br /&gt;
				e.preventDefault();&lt;br /&gt;
				e.stopPropagation();&lt;br /&gt;
				$( this ).removeClass( &amp;#039;ve-bulkupload-dropzone-active&amp;#039; );&lt;br /&gt;
				var files = e.originalEvent.dataTransfer &amp;amp;&amp;amp; e.originalEvent.dataTransfer.files;&lt;br /&gt;
				if ( files &amp;amp;&amp;amp; files.length ) {&lt;br /&gt;
					dialog.addFiles( Array.prototype.slice.call( files ) );&lt;br /&gt;
				}&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
		this.$fileList = $( &amp;#039;&amp;lt;div&amp;gt;&amp;#039; ).addClass( &amp;#039;ve-bulkupload-files&amp;#039; );&lt;br /&gt;
&lt;br /&gt;
		// Shared options&lt;br /&gt;
		this.insertAsInput = new OO.ui.RadioSelectInputWidget( {&lt;br /&gt;
			options: [&lt;br /&gt;
				{ data: &amp;#039;gallery&amp;#039;, label: &amp;#039;A gallery&amp;#039; },&lt;br /&gt;
				{ data: &amp;#039;thumbs&amp;#039;, label: &amp;#039;Individual thumbnails&amp;#039; },&lt;br /&gt;
				{ data: &amp;#039;links&amp;#039;, label: &amp;#039;A list of file links&amp;#039; }&lt;br /&gt;
			],&lt;br /&gt;
			value: &amp;#039;gallery&amp;#039;&lt;br /&gt;
		} );&lt;br /&gt;
		this.insertAsInput.connect( this, { change: &amp;#039;onInsertAsChange&amp;#039; } );&lt;br /&gt;
&lt;br /&gt;
		this.heightsInput = new OO.ui.NumberInputWidget( { value: 180, min: 60, max: 500 } );&lt;br /&gt;
		this.captionsInput = new OO.ui.CheckboxInputWidget( { selected: true } );&lt;br /&gt;
		this.descriptionInput = new OO.ui.MultilineTextInputWidget( {&lt;br /&gt;
			rows: 2,&lt;br /&gt;
			placeholder: &amp;#039;Shared description saved on every file page (optional)&amp;#039;&lt;br /&gt;
		} );&lt;br /&gt;
		this.categoriesInput = new mw.widgets.CategorySelector();&lt;br /&gt;
&lt;br /&gt;
		this.galleryField = new OO.ui.FieldLayout( this.heightsInput, {&lt;br /&gt;
			label: &amp;#039;Gallery image height (px)&amp;#039;,&lt;br /&gt;
			align: &amp;#039;top&amp;#039;&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		this.optionsFieldset = new OO.ui.FieldsetLayout( { label: &amp;#039;Insert as&amp;#039; } );&lt;br /&gt;
		this.optionsFieldset.addItems( [&lt;br /&gt;
			new OO.ui.FieldLayout( this.insertAsInput, { align: &amp;#039;top&amp;#039; } ),&lt;br /&gt;
			this.galleryField,&lt;br /&gt;
			new OO.ui.FieldLayout( this.captionsInput, {&lt;br /&gt;
				label: &amp;#039;Use file names as captions&amp;#039;,&lt;br /&gt;
				align: &amp;#039;inline&amp;#039;&lt;br /&gt;
			} ),&lt;br /&gt;
			new OO.ui.FieldLayout( this.descriptionInput, {&lt;br /&gt;
				label: &amp;#039;Description&amp;#039;,&lt;br /&gt;
				align: &amp;#039;top&amp;#039;&lt;br /&gt;
			} ),&lt;br /&gt;
			new OO.ui.FieldLayout( this.categoriesInput, {&lt;br /&gt;
				label: &amp;#039;Categories for every file&amp;#039;,&lt;br /&gt;
				align: &amp;#039;top&amp;#039;&lt;br /&gt;
			} )&lt;br /&gt;
		] );&lt;br /&gt;
&lt;br /&gt;
		// Progress + results&lt;br /&gt;
		this.overallProgress = new OO.ui.ProgressBarWidget( { progress: 0 } );&lt;br /&gt;
		this.overallProgress.toggle( false );&lt;br /&gt;
		this.statusLabel = new OO.ui.LabelWidget( {&lt;br /&gt;
			label: &amp;#039;&amp;#039;,&lt;br /&gt;
			classes: [ &amp;#039;ve-bulkupload-status&amp;#039; ]&lt;br /&gt;
		} );&lt;br /&gt;
		this.$results = $( &amp;#039;&amp;lt;div&amp;gt;&amp;#039; ).addClass( &amp;#039;ve-bulkupload-results&amp;#039; ).hide();&lt;br /&gt;
&lt;br /&gt;
		this.panel.$element.append(&lt;br /&gt;
			this.$dropZone,&lt;br /&gt;
			this.$fileList,&lt;br /&gt;
			this.optionsFieldset.$element,&lt;br /&gt;
			this.overallProgress.$element,&lt;br /&gt;
			this.statusLabel.$element,&lt;br /&gt;
			this.$results&lt;br /&gt;
		);&lt;br /&gt;
		this.$body.append( this.panel.$element );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	BulkUploadDialog.prototype.onInsertAsChange = function () {&lt;br /&gt;
		this.galleryField.toggle( this.insertAsInput.getValue() === &amp;#039;gallery&amp;#039; );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	BulkUploadDialog.prototype.getSetupProcess = function ( data ) {&lt;br /&gt;
		data = data || {};&lt;br /&gt;
		return BulkUploadDialog.super.prototype.getSetupProcess.call( this, data )&lt;br /&gt;
			.next( function () {&lt;br /&gt;
				this.rows = [];&lt;br /&gt;
				this.results = [];&lt;br /&gt;
				this.failures = [];&lt;br /&gt;
				this.aborted = false;&lt;br /&gt;
				this.currentUpload = null;&lt;br /&gt;
&lt;br /&gt;
				this.$fileList.empty();&lt;br /&gt;
				this.$results.empty().hide();&lt;br /&gt;
				this.overallProgress.toggle( false ).setProgress( 0 );&lt;br /&gt;
				this.statusLabel.setLabel( &amp;#039;&amp;#039; );&lt;br /&gt;
				this.descriptionInput.setValue( &amp;#039;&amp;#039; );&lt;br /&gt;
				this.categoriesInput.setItemsFromData( [] );&lt;br /&gt;
				this.insertAsInput.setValue( &amp;#039;gallery&amp;#039; );&lt;br /&gt;
				this.captionsInput.setSelected( true );&lt;br /&gt;
				this.onInsertAsChange();&lt;br /&gt;
				this.actions.setMode( &amp;#039;edit&amp;#039; );&lt;br /&gt;
&lt;br /&gt;
				if ( data.files &amp;amp;&amp;amp; data.files.length ) {&lt;br /&gt;
					this.addFiles( data.files );&lt;br /&gt;
				}&lt;br /&gt;
				this.updateActions();&lt;br /&gt;
			}, this );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	BulkUploadDialog.prototype.getTeardownProcess = function ( data ) {&lt;br /&gt;
		return BulkUploadDialog.super.prototype.getTeardownProcess.call( this, data )&lt;br /&gt;
			.first( function () {&lt;br /&gt;
				this.rows.forEach( function ( row ) {&lt;br /&gt;
					row.destroy();&lt;br /&gt;
				} );&lt;br /&gt;
				this.rows = [];&lt;br /&gt;
			}, this );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	BulkUploadDialog.prototype.addFiles = function ( files ) {&lt;br /&gt;
		var dialog = this;&lt;br /&gt;
&lt;br /&gt;
		getAllowedExtensions().then( function ( allowed ) {&lt;br /&gt;
			var rejected = [];&lt;br /&gt;
&lt;br /&gt;
			files.forEach( function ( file ) {&lt;br /&gt;
				var ext = getExtension( file.name );&lt;br /&gt;
				if ( allowed.length &amp;amp;&amp;amp; allowed.indexOf( ext ) === -1 ) {&lt;br /&gt;
					rejected.push( file.name );&lt;br /&gt;
					return;&lt;br /&gt;
				}&lt;br /&gt;
				var row = new FileRow( file );&lt;br /&gt;
				row.connect( dialog, { remove: &amp;#039;onRowRemove&amp;#039;, nameChange: &amp;#039;onRowNameChange&amp;#039; } );&lt;br /&gt;
				dialog.rows.push( row );&lt;br /&gt;
				dialog.$fileList.append( row.$element );&lt;br /&gt;
			} );&lt;br /&gt;
&lt;br /&gt;
			if ( rejected.length ) {&lt;br /&gt;
				dialog.statusLabel.setLabel(&lt;br /&gt;
					&amp;#039;Not allowed on this wiki, skipped: &amp;#039; + rejected.join( &amp;#039;, &amp;#039; )&lt;br /&gt;
				);&lt;br /&gt;
			}&lt;br /&gt;
			dialog.updateActions();&lt;br /&gt;
			dialog.checkExisting();&lt;br /&gt;
			dialog.updateSize();&lt;br /&gt;
		} );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	BulkUploadDialog.prototype.onRowRemove = function ( row ) {&lt;br /&gt;
		var index = this.rows.indexOf( row );&lt;br /&gt;
		if ( index !== -1 ) {&lt;br /&gt;
			this.rows.splice( index, 1 );&lt;br /&gt;
			row.$element.remove();&lt;br /&gt;
			row.destroy();&lt;br /&gt;
			this.updateActions();&lt;br /&gt;
			this.updateSize();&lt;br /&gt;
		}&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	BulkUploadDialog.prototype.onRowNameChange = function () {&lt;br /&gt;
		if ( this.nameCheckTimeout ) {&lt;br /&gt;
			clearTimeout( this.nameCheckTimeout );&lt;br /&gt;
		}&lt;br /&gt;
		this.nameCheckTimeout = setTimeout( this.checkExisting.bind( this ), 500 );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	BulkUploadDialog.prototype.updateActions = function () {&lt;br /&gt;
		this.actions.setAbilities( { upload: this.rows.length &amp;gt; 0 } );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	/**&lt;br /&gt;
	 * Ask the wiki which of the queued names are already taken, so conflicts are&lt;br /&gt;
	 * resolved before a long upload run rather than in the middle of one.&lt;br /&gt;
	 */&lt;br /&gt;
	BulkUploadDialog.prototype.checkExisting = function () {&lt;br /&gt;
		var dialog = this,&lt;br /&gt;
			rows = this.rows.slice(),&lt;br /&gt;
			titles = rows.map( function ( row ) {&lt;br /&gt;
				return &amp;#039;File:&amp;#039; + row.getFileName();&lt;br /&gt;
			} ),&lt;br /&gt;
			chunks = [],&lt;br /&gt;
			i;&lt;br /&gt;
&lt;br /&gt;
		if ( !titles.length ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
		for ( i = 0; i &amp;lt; titles.length; i += TITLE_QUERY_CHUNK ) {&lt;br /&gt;
			chunks.push( titles.slice( i, i + TITLE_QUERY_CHUNK ) );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		$.when.apply( $, chunks.map( function ( chunk ) {&lt;br /&gt;
			return getApi().get( {&lt;br /&gt;
				action: &amp;#039;query&amp;#039;,&lt;br /&gt;
				titles: chunk,&lt;br /&gt;
				formatversion: 2&lt;br /&gt;
			} );&lt;br /&gt;
		} ) ).then( function () {&lt;br /&gt;
			var existing = {};&lt;br /&gt;
			Array.prototype.slice.call( arguments ).forEach( function ( response ) {&lt;br /&gt;
				var data = Array.isArray( response ) ? response[ 0 ] : response;&lt;br /&gt;
				( ( data &amp;amp;&amp;amp; data.query &amp;amp;&amp;amp; data.query.pages ) || [] ).forEach( function ( page ) {&lt;br /&gt;
					if ( !page.missing ) {&lt;br /&gt;
						existing[ page.title ] = true;&lt;br /&gt;
					}&lt;br /&gt;
				} );&lt;br /&gt;
			} );&lt;br /&gt;
			rows.forEach( function ( row ) {&lt;br /&gt;
				if ( dialog.rows.indexOf( row ) !== -1 &amp;amp;&amp;amp; !row.done ) {&lt;br /&gt;
					row.setExists( !!existing[ &amp;#039;File:&amp;#039; + row.getFileName() ] );&lt;br /&gt;
				}&lt;br /&gt;
			} );&lt;br /&gt;
			dialog.updateSize();&lt;br /&gt;
		} );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	BulkUploadDialog.prototype.getActionProcess = function ( action ) {&lt;br /&gt;
		var dialog = this;&lt;br /&gt;
&lt;br /&gt;
		if ( action === &amp;#039;upload&amp;#039; ) {&lt;br /&gt;
			return new OO.ui.Process( function () {&lt;br /&gt;
				var error = dialog.validate();&lt;br /&gt;
				if ( error ) {&lt;br /&gt;
					return $.Deferred().reject( new OO.ui.Error( error, { recoverable: true } ) ).promise();&lt;br /&gt;
				}&lt;br /&gt;
				dialog.actions.setMode( &amp;#039;uploading&amp;#039; );&lt;br /&gt;
				dialog.$dropZone.hide();&lt;br /&gt;
				dialog.optionsFieldset.$element.hide();&lt;br /&gt;
				dialog.overallProgress.toggle( true );&lt;br /&gt;
				dialog.rows.forEach( function ( row ) {&lt;br /&gt;
					row.setBusy( true );&lt;br /&gt;
				} );&lt;br /&gt;
				return dialog.runUploads().then( function () {&lt;br /&gt;
					return dialog.finish();&lt;br /&gt;
				} );&lt;br /&gt;
			} );&lt;br /&gt;
		}&lt;br /&gt;
		if ( action === &amp;#039;abort&amp;#039; ) {&lt;br /&gt;
			return new OO.ui.Process( function () {&lt;br /&gt;
				dialog.aborted = true;&lt;br /&gt;
				dialog.statusLabel.setLabel( &amp;#039;Stopping…&amp;#039; );&lt;br /&gt;
				if ( dialog.currentUpload &amp;amp;&amp;amp; dialog.currentUpload.abort ) {&lt;br /&gt;
					dialog.currentUpload.abort();&lt;br /&gt;
				}&lt;br /&gt;
			} );&lt;br /&gt;
		}&lt;br /&gt;
		if ( action === &amp;#039;finish&amp;#039; ) {&lt;br /&gt;
			return new OO.ui.Process( function () {&lt;br /&gt;
				dialog.close( { action: &amp;#039;finish&amp;#039; } );&lt;br /&gt;
			} );&lt;br /&gt;
		}&lt;br /&gt;
		return BulkUploadDialog.super.prototype.getActionProcess.call( this, action );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	BulkUploadDialog.prototype.validate = function () {&lt;br /&gt;
		var seen = {},&lt;br /&gt;
			problem = null;&lt;br /&gt;
&lt;br /&gt;
		if ( !this.rows.length ) {&lt;br /&gt;
			return &amp;#039;Add at least one file.&amp;#039;;&lt;br /&gt;
		}&lt;br /&gt;
		this.rows.forEach( function ( row ) {&lt;br /&gt;
			var name = row.getFileName();&lt;br /&gt;
			if ( !name || name === &amp;#039;.&amp;#039; + getExtension( name ) ) {&lt;br /&gt;
				problem = problem || &amp;#039;Every file needs a name.&amp;#039;;&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
			if ( !getExtension( name ) ) {&lt;br /&gt;
				problem = problem || &amp;#039;File names must keep their extension (&amp;#039; + name + &amp;#039;).&amp;#039;;&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
			if ( seen[ name ] &amp;amp;&amp;amp; row.getPolicy() !== &amp;#039;skip&amp;#039; ) {&lt;br /&gt;
				problem = problem || &amp;#039;Two files are queued as &amp;quot;&amp;#039; + name + &amp;#039;&amp;quot;. Rename one of them.&amp;#039;;&lt;br /&gt;
			}&lt;br /&gt;
			seen[ name ] = true;&lt;br /&gt;
		} );&lt;br /&gt;
		return problem;&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	/** Upload every queued file, one at a time. Never rejects. */&lt;br /&gt;
	BulkUploadDialog.prototype.runUploads = function () {&lt;br /&gt;
		var dialog = this,&lt;br /&gt;
			queue = this.rows.filter( function ( row ) {&lt;br /&gt;
				return row.getPolicy() !== &amp;#039;skip&amp;#039;;&lt;br /&gt;
			} ),&lt;br /&gt;
			total = queue.length,&lt;br /&gt;
			index = 0;&lt;br /&gt;
&lt;br /&gt;
		function next() {&lt;br /&gt;
			var row;&lt;br /&gt;
			if ( dialog.aborted || index &amp;gt;= total ) {&lt;br /&gt;
				return $.Deferred().resolve().promise();&lt;br /&gt;
			}&lt;br /&gt;
			row = queue[ index ];&lt;br /&gt;
			dialog.statusLabel.setLabel(&lt;br /&gt;
				&amp;#039;Uploading &amp;#039; + ( index + 1 ) + &amp;#039; of &amp;#039; + total + &amp;#039;: &amp;#039; + row.getFileName()&lt;br /&gt;
			);&lt;br /&gt;
			return dialog.uploadRow( row, index, total ).then( function () {&lt;br /&gt;
				index++;&lt;br /&gt;
				dialog.overallProgress.setProgress( Math.round( ( index / total ) * 100 ) );&lt;br /&gt;
				return next();&lt;br /&gt;
			} );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		return next();&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	/** Upload a single row. Always resolves; failures are recorded on the row. */&lt;br /&gt;
	BulkUploadDialog.prototype.uploadRow = function ( row, index, total ) {&lt;br /&gt;
		var dialog = this,&lt;br /&gt;
			deferred = $.Deferred(),&lt;br /&gt;
			name = row.getFileName(),&lt;br /&gt;
			policy = row.getPolicy(),&lt;br /&gt;
			useCaptions = this.captionsInput.isSelected(),&lt;br /&gt;
			caption = useCaptions ? captionFor( row.file.name ) : &amp;#039;&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
		function record( finalName, note ) {&lt;br /&gt;
			row.done = true;&lt;br /&gt;
			row.uploadedName = finalName;&lt;br /&gt;
			row.setProgress( 1 );&lt;br /&gt;
			row.setStatus( note || &amp;#039;Uploaded.&amp;#039;, &amp;#039;done&amp;#039; );&lt;br /&gt;
			dialog.results.push( {&lt;br /&gt;
				name: finalName,&lt;br /&gt;
				caption: caption,&lt;br /&gt;
				isImage: isImageName( finalName )&lt;br /&gt;
			} );&lt;br /&gt;
			deferred.resolve();&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		function fail( message ) {&lt;br /&gt;
			row.setStatus( message, &amp;#039;error&amp;#039; );&lt;br /&gt;
			dialog.failures.push( { name: name, message: message } );&lt;br /&gt;
			deferred.resolve();&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( policy === &amp;#039;existing&amp;#039; ) {&lt;br /&gt;
			record( name, &amp;#039;Used the file already on the wiki.&amp;#039; );&lt;br /&gt;
			return deferred.promise();&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( policy === &amp;#039;rename&amp;#039; &amp;amp;&amp;amp; row.exists ) {&lt;br /&gt;
			name = this.uniqueName( name );&lt;br /&gt;
			row.nameInput.setValue( name );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		this.currentUpload = getApi().upload( row.file, {&lt;br /&gt;
			filename: name,&lt;br /&gt;
			comment: &amp;#039;Bulk upload from the visual editor&amp;#039;,&lt;br /&gt;
			text: this.buildFilePageText( caption ),&lt;br /&gt;
			ignorewarnings: policy === &amp;#039;overwrite&amp;#039; ? 1 : undefined&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		this.currentUpload.progress( function ( fraction ) {&lt;br /&gt;
			row.setProgress( fraction );&lt;br /&gt;
			dialog.overallProgress.setProgress(&lt;br /&gt;
				Math.round( ( ( index + fraction ) / total ) * 100 )&lt;br /&gt;
			);&lt;br /&gt;
		} ).done( function ( result ) {&lt;br /&gt;
			var uploaded = result &amp;amp;&amp;amp; result.upload;&lt;br /&gt;
			record( ( uploaded &amp;amp;&amp;amp; uploaded.filename ) || name );&lt;br /&gt;
		} ).fail( function ( code, result ) {&lt;br /&gt;
			var warnings = result &amp;amp;&amp;amp; result.upload &amp;amp;&amp;amp; result.upload.warnings;&lt;br /&gt;
&lt;br /&gt;
			if ( dialog.aborted ) {&lt;br /&gt;
				row.setStatus( &amp;#039;Cancelled.&amp;#039;, &amp;#039;warning&amp;#039; );&lt;br /&gt;
				deferred.resolve();&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
			if ( warnings &amp;amp;&amp;amp; warnings.duplicate &amp;amp;&amp;amp; warnings.duplicate.length ) {&lt;br /&gt;
				// Byte-identical file already on the wiki — use it instead of&lt;br /&gt;
				// storing a second copy.&lt;br /&gt;
				record( warnings.duplicate[ 0 ], &amp;#039;Identical file already on the wiki — used that one.&amp;#039; );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
			if ( warnings &amp;amp;&amp;amp; ( warnings.exists || warnings[ &amp;#039;exists-normalized&amp;#039; ] ) ) {&lt;br /&gt;
				fail( &amp;#039;A file called &amp;quot;&amp;#039; + name + &amp;#039;&amp;quot; already exists. Choose a different name, or &amp;quot;Overwrite&amp;quot;.&amp;#039; );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
			if ( warnings &amp;amp;&amp;amp; warnings[ &amp;#039;was-deleted&amp;#039; ] ) {&lt;br /&gt;
				fail( &amp;#039;A file with this name was deleted before. Rename it, or upload it via Special:Upload.&amp;#039; );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
			if ( warnings ) {&lt;br /&gt;
				fail( &amp;#039;Upload warning: &amp;#039; + Object.keys( warnings ).join( &amp;#039;, &amp;#039; ) );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
			fail( apiErrorMessage( code, result ) );&lt;br /&gt;
		} ).always( function () {&lt;br /&gt;
			dialog.currentUpload = null;&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		return deferred.promise();&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	/** Find a free &amp;quot;Name-1.jpg&amp;quot; style variant. */&lt;br /&gt;
	BulkUploadDialog.prototype.uniqueName = function ( name ) {&lt;br /&gt;
		var base = stripExtension( name ),&lt;br /&gt;
			ext = getExtension( name ),&lt;br /&gt;
			taken = {},&lt;br /&gt;
			candidate,&lt;br /&gt;
			i = 1;&lt;br /&gt;
&lt;br /&gt;
		this.rows.forEach( function ( row ) {&lt;br /&gt;
			if ( row.uploadedName ) {&lt;br /&gt;
				taken[ row.uploadedName ] = true;&lt;br /&gt;
			}&lt;br /&gt;
		} );&lt;br /&gt;
		this.results.forEach( function ( result ) {&lt;br /&gt;
			taken[ result.name ] = true;&lt;br /&gt;
		} );&lt;br /&gt;
&lt;br /&gt;
		do {&lt;br /&gt;
			candidate = base + &amp;#039;-&amp;#039; + i + &amp;#039;.&amp;#039; + ext;&lt;br /&gt;
			i++;&lt;br /&gt;
		} while ( taken[ candidate ] &amp;amp;&amp;amp; i &amp;lt; 1000 );&lt;br /&gt;
&lt;br /&gt;
		return candidate;&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	BulkUploadDialog.prototype.buildFilePageText = function ( caption ) {&lt;br /&gt;
		var parts = [],&lt;br /&gt;
			description = this.descriptionInput.getValue().trim(),&lt;br /&gt;
			categories = this.categoriesInput.getItemsData();&lt;br /&gt;
&lt;br /&gt;
		if ( description ) {&lt;br /&gt;
			parts.push( description );&lt;br /&gt;
		} else if ( caption ) {&lt;br /&gt;
			parts.push( caption );&lt;br /&gt;
		}&lt;br /&gt;
		if ( categories.length ) {&lt;br /&gt;
			parts.push( categories.map( function ( category ) {&lt;br /&gt;
				return &amp;#039;[[Category:&amp;#039; + category + &amp;#039;]]&amp;#039;;&lt;br /&gt;
			} ).join( &amp;#039;\n&amp;#039; ) );&lt;br /&gt;
		}&lt;br /&gt;
		return parts.join( &amp;#039;\n\n&amp;#039; );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	/** Build the wikitext for everything that uploaded successfully. */&lt;br /&gt;
	BulkUploadDialog.prototype.buildWikitext = function () {&lt;br /&gt;
		var insertAs = this.insertAsInput.getValue(),&lt;br /&gt;
			heights = parseInt( this.heightsInput.getValue(), 10 ) || 180,&lt;br /&gt;
			images = this.results.filter( function ( r ) {&lt;br /&gt;
				return r.isImage;&lt;br /&gt;
			} ),&lt;br /&gt;
			others = this.results.filter( function ( r ) {&lt;br /&gt;
				return !r.isImage;&lt;br /&gt;
			} ),&lt;br /&gt;
			parts = [];&lt;br /&gt;
&lt;br /&gt;
		function link( result ) {&lt;br /&gt;
			return &amp;#039;* [[Media:&amp;#039; + result.name + &amp;#039;|&amp;#039; + ( result.caption || result.name ) + &amp;#039;]]&amp;#039;;&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( images.length ) {&lt;br /&gt;
			if ( insertAs === &amp;#039;gallery&amp;#039; ) {&lt;br /&gt;
				parts.push(&lt;br /&gt;
					&amp;#039;&amp;lt;gallery mode=&amp;quot;packed&amp;quot; heights=&amp;quot;&amp;#039; + heights + &amp;#039;&amp;quot;&amp;gt;\n&amp;#039; +&lt;br /&gt;
					images.map( function ( result ) {&lt;br /&gt;
						return &amp;#039;File:&amp;#039; + result.name + ( result.caption ? &amp;#039;|&amp;#039; + result.caption : &amp;#039;&amp;#039; );&lt;br /&gt;
					} ).join( &amp;#039;\n&amp;#039; ) +&lt;br /&gt;
					&amp;#039;\n&amp;lt;/gallery&amp;gt;&amp;#039;&lt;br /&gt;
				);&lt;br /&gt;
			} else if ( insertAs === &amp;#039;thumbs&amp;#039; ) {&lt;br /&gt;
				parts.push( images.map( function ( result ) {&lt;br /&gt;
					return &amp;#039;[[File:&amp;#039; + result.name + &amp;#039;|thumb|&amp;#039; + ( result.caption || &amp;#039;&amp;#039; ) + &amp;#039;]]&amp;#039;;&lt;br /&gt;
				} ).join( &amp;#039;\n&amp;#039; ) );&lt;br /&gt;
			} else {&lt;br /&gt;
				parts.push( images.map( link ).join( &amp;#039;\n&amp;#039; ) );&lt;br /&gt;
			}&lt;br /&gt;
		}&lt;br /&gt;
		if ( others.length ) {&lt;br /&gt;
			parts.push( others.map( link ).join( &amp;#039;\n&amp;#039; ) );&lt;br /&gt;
		}&lt;br /&gt;
		return parts.join( &amp;#039;\n\n&amp;#039; );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	/**&lt;br /&gt;
	 * Insert wikitext at the target fragment, reusing VisualEditor&amp;#039;s own&lt;br /&gt;
	 * wikitext → document conversion (same path as pasting wikitext).&lt;br /&gt;
	 *&lt;br /&gt;
	 * @param {string} wikitext&lt;br /&gt;
	 * @return {jQuery.Promise}&lt;br /&gt;
	 */&lt;br /&gt;
	BulkUploadDialog.prototype.insertWikitext = function ( wikitext ) {&lt;br /&gt;
		var fragment = this.getFragment(),&lt;br /&gt;
			surface = ( this.manager &amp;amp;&amp;amp; this.manager.getSurface &amp;amp;&amp;amp; this.manager.getSurface() ) ||&lt;br /&gt;
				( ve.init.target &amp;amp;&amp;amp; ve.init.target.getSurface() ),&lt;br /&gt;
			item;&lt;br /&gt;
&lt;br /&gt;
		if ( !surface || !fragment ) {&lt;br /&gt;
			return $.Deferred().reject( &amp;#039;no surface&amp;#039; ).promise();&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		item = ve.ui.DataTransferItem.static.newFromString( wikitext, &amp;#039;text/plain&amp;#039; );&lt;br /&gt;
		return ve.ui.dataTransferHandlerFactory.create( &amp;#039;wikitextString&amp;#039;, surface, item )&lt;br /&gt;
			.getInsertableData()&lt;br /&gt;
			.then( function ( docOrData ) {&lt;br /&gt;
				var target = fragment.collapseToEnd();&lt;br /&gt;
				if ( docOrData instanceof ve.dm.Document ) {&lt;br /&gt;
					target.insertDocument( docOrData );&lt;br /&gt;
				} else {&lt;br /&gt;
					target.insertContent( docOrData );&lt;br /&gt;
				}&lt;br /&gt;
				target.collapseToEnd().select();&lt;br /&gt;
			} );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	/** Insert what succeeded, then close or report. */&lt;br /&gt;
	BulkUploadDialog.prototype.finish = function () {&lt;br /&gt;
		var dialog = this,&lt;br /&gt;
			wikitext;&lt;br /&gt;
&lt;br /&gt;
		this.overallProgress.setProgress( 100 );&lt;br /&gt;
&lt;br /&gt;
		if ( this.aborted ) {&lt;br /&gt;
			this.statusLabel.setLabel( &amp;#039;Upload stopped — nothing was inserted.&amp;#039; );&lt;br /&gt;
			this.showResults( true );&lt;br /&gt;
			return $.Deferred().resolve().promise();&lt;br /&gt;
		}&lt;br /&gt;
		if ( !this.results.length ) {&lt;br /&gt;
			this.statusLabel.setLabel( &amp;#039;Nothing was uploaded.&amp;#039; );&lt;br /&gt;
			this.showResults( false );&lt;br /&gt;
			return $.Deferred().resolve().promise();&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		this.statusLabel.setLabel( &amp;#039;Inserting…&amp;#039; );&lt;br /&gt;
		wikitext = this.buildWikitext();&lt;br /&gt;
&lt;br /&gt;
		return this.insertWikitext( wikitext ).then( function () {&lt;br /&gt;
			if ( dialog.failures.length ) {&lt;br /&gt;
				dialog.statusLabel.setLabel( &amp;#039;Inserted, but some files failed.&amp;#039; );&lt;br /&gt;
				dialog.showResults( false );&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
			dialog.close( { action: &amp;#039;inserted&amp;#039; } );&lt;br /&gt;
		}, function () {&lt;br /&gt;
			// Insertion failed — the files are uploaded, so hand over the wikitext.&lt;br /&gt;
			dialog.statusLabel.setLabel( &amp;#039;The files uploaded, but could not be inserted automatically.&amp;#039; );&lt;br /&gt;
			dialog.showResults( false, wikitext );&lt;br /&gt;
		} );&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	BulkUploadDialog.prototype.showResults = function ( aborted, wikitext ) {&lt;br /&gt;
		var $list = $( &amp;#039;&amp;lt;ul&amp;gt;&amp;#039; );&lt;br /&gt;
&lt;br /&gt;
		this.actions.setMode( &amp;#039;result&amp;#039; );&lt;br /&gt;
		this.$results.empty().show();&lt;br /&gt;
&lt;br /&gt;
		if ( this.results.length ) {&lt;br /&gt;
			this.$results.append( $( &amp;#039;&amp;lt;p&amp;gt;&amp;#039; ).text(&lt;br /&gt;
				aborted ?&lt;br /&gt;
					&amp;#039;Uploaded before stopping (not inserted — you can find them under Special:NewFiles):&amp;#039; :&lt;br /&gt;
					&amp;#039;Uploaded:&amp;#039;&lt;br /&gt;
			) );&lt;br /&gt;
			this.results.forEach( function ( result ) {&lt;br /&gt;
				$list.append( $( &amp;#039;&amp;lt;li&amp;gt;&amp;#039; ).append(&lt;br /&gt;
					$( &amp;#039;&amp;lt;a&amp;gt;&amp;#039; )&lt;br /&gt;
						.attr( { href: mw.util.getUrl( &amp;#039;File:&amp;#039; + result.name ), target: &amp;#039;_blank&amp;#039; } )&lt;br /&gt;
						.text( result.name )&lt;br /&gt;
				) );&lt;br /&gt;
			} );&lt;br /&gt;
			this.$results.append( $list );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( this.failures.length ) {&lt;br /&gt;
			this.$results.append( $( &amp;#039;&amp;lt;p&amp;gt;&amp;#039; ).addClass( &amp;#039;ve-bulkupload-status-error&amp;#039; ).text( &amp;#039;Failed:&amp;#039; ) );&lt;br /&gt;
			this.$results.append( $( &amp;#039;&amp;lt;ul&amp;gt;&amp;#039; ).append( this.failures.map( function ( failure ) {&lt;br /&gt;
				return $( &amp;#039;&amp;lt;li&amp;gt;&amp;#039; ).text( failure.name + &amp;#039; — &amp;#039; + failure.message );&lt;br /&gt;
			} ) ) );&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		if ( wikitext ) {&lt;br /&gt;
			this.$results.append(&lt;br /&gt;
				$( &amp;#039;&amp;lt;p&amp;gt;&amp;#039; ).text( &amp;#039;Copy this into the page yourself:&amp;#039; ),&lt;br /&gt;
				$( &amp;#039;&amp;lt;pre&amp;gt;&amp;#039; ).text( wikitext )&lt;br /&gt;
			);&lt;br /&gt;
		}&lt;br /&gt;
&lt;br /&gt;
		this.updateSize();&lt;br /&gt;
	};&lt;br /&gt;
&lt;br /&gt;
	ve.ui.windowFactory.register( BulkUploadDialog );&lt;br /&gt;
&lt;br /&gt;
	/* --------------------------------------------------------- command/tool -- */&lt;br /&gt;
&lt;br /&gt;
	ve.ui.commandRegistry.register(&lt;br /&gt;
		new ve.ui.Command( &amp;#039;bulkUpload&amp;#039;, &amp;#039;window&amp;#039;, &amp;#039;open&amp;#039;, { args: [ &amp;#039;veBulkUpload&amp;#039; ] } )&lt;br /&gt;
	);&lt;br /&gt;
&lt;br /&gt;
	/**&lt;br /&gt;
	 * @class&lt;br /&gt;
	 */&lt;br /&gt;
	function BulkUploadTool() {&lt;br /&gt;
		BulkUploadTool.super.apply( this, arguments );&lt;br /&gt;
	}&lt;br /&gt;
	OO.inheritClass( BulkUploadTool, ve.ui.Tool );&lt;br /&gt;
	BulkUploadTool.static.name = &amp;#039;bulkUpload&amp;#039;;&lt;br /&gt;
	BulkUploadTool.static.group = &amp;#039;object&amp;#039;;&lt;br /&gt;
	BulkUploadTool.static.icon = &amp;#039;imageGallery&amp;#039;;&lt;br /&gt;
	BulkUploadTool.static.title = &amp;#039;Upload files&amp;#039;;&lt;br /&gt;
	BulkUploadTool.static.commandName = &amp;#039;bulkUpload&amp;#039;;&lt;br /&gt;
	ve.ui.toolFactory.register( BulkUploadTool );&lt;br /&gt;
&lt;br /&gt;
	/* ----------------------------------------------------------- drop hook -- */&lt;br /&gt;
&lt;br /&gt;
	var $toast = null,&lt;br /&gt;
		toastTimeout = null;&lt;br /&gt;
&lt;br /&gt;
	function showToast( text ) {&lt;br /&gt;
		if ( !$toast ) {&lt;br /&gt;
			$toast = $( &amp;#039;&amp;lt;div&amp;gt;&amp;#039; ).addClass( &amp;#039;ve-bulkupload-toast&amp;#039; ).appendTo( document.body );&lt;br /&gt;
		}&lt;br /&gt;
		$toast.text( text ).addClass( &amp;#039;ve-bulkupload-toast-visible&amp;#039; );&lt;br /&gt;
		if ( toastTimeout ) {&lt;br /&gt;
			clearTimeout( toastTimeout );&lt;br /&gt;
		}&lt;br /&gt;
		toastTimeout = setTimeout( hideToast, 2500 );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function hideToast() {&lt;br /&gt;
		if ( $toast ) {&lt;br /&gt;
			$toast.removeClass( &amp;#039;ve-bulkupload-toast-visible&amp;#039; );&lt;br /&gt;
		}&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function countDraggedFiles( dataTransfer ) {&lt;br /&gt;
		if ( !dataTransfer ) {&lt;br /&gt;
			return 0;&lt;br /&gt;
		}&lt;br /&gt;
		if ( dataTransfer.items &amp;amp;&amp;amp; dataTransfer.items.length ) {&lt;br /&gt;
			return Array.prototype.filter.call( dataTransfer.items, function ( item ) {&lt;br /&gt;
				return item.kind === &amp;#039;file&amp;#039;;&lt;br /&gt;
			} ).length;&lt;br /&gt;
		}&lt;br /&gt;
		return dataTransfer.files ? dataTransfer.files.length : 0;&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	function attachDropHandlers( surface ) {&lt;br /&gt;
		var view = surface.getView(),&lt;br /&gt;
			element = view.$element[ 0 ];&lt;br /&gt;
&lt;br /&gt;
		if ( !element || element.veBulkUploadAttached ) {&lt;br /&gt;
			return;&lt;br /&gt;
		}&lt;br /&gt;
		element.veBulkUploadAttached = true;&lt;br /&gt;
&lt;br /&gt;
		element.addEventListener( &amp;#039;dragover&amp;#039;, function ( e ) {&lt;br /&gt;
			var count = countDraggedFiles( e.dataTransfer );&lt;br /&gt;
			if ( count &amp;gt; 1 ) {&lt;br /&gt;
				showToast( &amp;#039;Drop &amp;#039; + count + &amp;#039; files to upload them all&amp;#039; );&lt;br /&gt;
			}&lt;br /&gt;
		}, true );&lt;br /&gt;
&lt;br /&gt;
		element.addEventListener( &amp;#039;dragleave&amp;#039;, hideToast, true );&lt;br /&gt;
&lt;br /&gt;
		element.addEventListener( &amp;#039;drop&amp;#039;, function ( e ) {&lt;br /&gt;
			var files = e.dataTransfer &amp;amp;&amp;amp; e.dataTransfer.files,&lt;br /&gt;
				model = surface.getModel(),&lt;br /&gt;
				offset,&lt;br /&gt;
				fragment;&lt;br /&gt;
&lt;br /&gt;
			hideToast();&lt;br /&gt;
&lt;br /&gt;
			// One file: leave it to VisualEditor&amp;#039;s own media dialog.&lt;br /&gt;
			if ( !files || files.length &amp;lt; 2 || surface.getMode() !== &amp;#039;visual&amp;#039; ) {&lt;br /&gt;
				return;&lt;br /&gt;
			}&lt;br /&gt;
&lt;br /&gt;
			e.preventDefault();&lt;br /&gt;
			e.stopPropagation();&lt;br /&gt;
&lt;br /&gt;
			offset = view.getOffsetFromEventCoords( e );&lt;br /&gt;
			if ( offset !== -1 &amp;amp;&amp;amp; offset !== undefined ) {&lt;br /&gt;
				model.setLinearSelection( new ve.Range( offset ) );&lt;br /&gt;
			}&lt;br /&gt;
			fragment = model.getFragment().collapseToEnd();&lt;br /&gt;
&lt;br /&gt;
			surface.execute( &amp;#039;window&amp;#039;, &amp;#039;open&amp;#039;, &amp;#039;veBulkUpload&amp;#039;, {&lt;br /&gt;
				files: Array.prototype.slice.call( files ),&lt;br /&gt;
				fragment: fragment&lt;br /&gt;
			} );&lt;br /&gt;
		}, true );&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	mw.hook( &amp;#039;ve.activationComplete&amp;#039; ).add( function () {&lt;br /&gt;
		var surface = ve.init.target &amp;amp;&amp;amp; ve.init.target.getSurface();&lt;br /&gt;
		if ( surface ) {&lt;br /&gt;
			attachDropHandlers( surface );&lt;br /&gt;
		}&lt;br /&gt;
	} );&lt;br /&gt;
&lt;br /&gt;
}() );&lt;/div&gt;</summary>
		<author><name>Hash</name></author>
	</entry>
</feed>