MediaWiki:Gadget-AjaxQuickDelete.js

Uwaga: aby zobaczyć zmiany po opublikowaniu, może zajść potrzeba wyczyszczenia pamięci podręcznej przeglądarki.

  • Firefox / Safari: Przytrzymaj Shift podczas klikania Odśwież bieżącą stronę, lub naciśnij klawisze Ctrl+F5, lub Ctrl+R (⌘-R na komputerze Mac)
  • Google Chrome: Naciśnij Ctrl-Shift-R (⌘-Shift-R na komputerze Mac)
  • Internet Explorer / Edge: Przytrzymaj Ctrl, jednocześnie klikając Odśwież, lub naciśnij klawisze Ctrl+F5
  • Opera: Naciśnij klawisze Ctrl+F5.
// Original code written by [[User:Ilmari Karonen]]
// Rewritten & extended by [[User:DieBuche]]. Botdetection and encoding fixer by [[User:Lupo]]
// Full list of authors of the original version:
//   http://commons.wikimedia.org/w/index.php?title=MediaWiki:AjaxQuickDelete.js&action=history
// Adapted from Commons for Polish Wikipedia by [[User:Lampak]]
// Adapted from plwiki for plwikinews by [[User:Msz2001]]
//
// Docs can be generated using JsDoc Toolkit: http://code.google.com/p/jsdoc-toolkit/
//
// Ajax-based replacement for [[commons:MediaWiki:Quick-delete-code.js]]
//
// TODO: better error handling/reporting
// <nowiki> <- Bez tego dziwne rzeczy się dzieją

if(typeof (AjaxQuickDelete) == 'undefined' && mw.config.get('wgNamespaceNumber') >= 0) {

	/**
	 * Main class of AjaxQuickDelete gadget.
	 *
	 * The gadget provides a user-friendly interface for making deletion requests.
	 *
	 * Dependencies: jquery, jquery.ui
	 *
	 * @constructor
	 */

	var local_wgPageName = {};
	local_wgPageName = mw.config.get('wgPageName');
	local_wgPageName = local_wgPageName.replace(/_/g, ' ');

	function AjaxQuickDeleteClass() {
		/**
		* Set up the AjaxQuickDelete object and add the sidebar link.
		*
		* Called when the page is loaded.
		*/
		this.install = function () {
			// Abort if the user opted out, or is using an antique skin & load the legacy version
			if(window.AjaxDeleteOptOut || ['vector-2022', 'vector', 'monobook', 'timeless'].indexOf(mw.config.get('skin')) === -1) {
				//importScript('MediaWiki:Quick-delete-code.js'); //not adpoted for pl.wiki
				return;
			}

			// Set up toolbox link
			// create toolbox link
			var portletId = mw.config.get('skin') === 'timeless' ? 'p-pagemisc' : 'p-tb';
			var portletLink = mw.util.addPortletLink(portletId, '#', this.i18n.toolboxLinkDelete, 't-ajaxquickdelete', null);
			// bind toolbox link
			$(portletLink).click(function (e) {
				e.preventDefault();
				AjaxQuickDelete.nominateForDeletion();
			});
			//! Usunięto wczytywanie listy wikiprojektów - nie ma ich na Wikinews
		};

		/**
		 * Open a dialogue window and set up basic variables.
		 *
		 * Called when the user clicked the sidebar link.
		 */
		this.nominateForDeletion = function () {
			this.startDate = new Date();

			// set up some page names we'll need later
			//this.requestPage = this.requestPagePrefix + '/' + this.formatDate("YYYY:MM:DD:") + local_wgPageName;
			//this.dailyLogPage = this.requestPagePrefix;

			this.section_line = '<!-- Nowe zgłoszenia wstawiaj poniżej tej linii. Nie usuwaj tej linii -->';

			// first schedule some API queries to fetch the info we need...
			this.tasks = [];  // reset task list in case an earlier error left it non-empty

			this.addTask('showProgress');
			this.addTask('findCreator');
			this.addTask('resolveRedirects');

			// ...then schedule the actual edits
			this.addTask('createRequestSubpage');
			this.addTask('prependTemplate');
			this.addTask('listRequestSubpage');
			this.addTask('notifyUploaders');
			this.addTask('notifyWikiproject');

			// finally reload the page to show the deletion tag
			this.addTask('reloadPage');

			// open the dialogue
			this.prompt();
		};

		/**
		 * Add another HTML select item so the user can choose one more
		 *   wikiproject to be notified.
		 *
		 * Called when the user clicked (+) button in the dialogue window.
		 */
		this.addWikiproject = function () {
			$('#ajax-delete-wikiproject-container').append(this.wikiproject_select.clone());
		};

		/**
		* Commence a process of adding a speedy deletion temptate to the
		*   page.
		*
		* Called when the Speedy deletion button has been clicked.
		*/
		this.speedyDeletionClicked = function (sender) {
			var reason = this.getAjaxQuestionAnswer("reason", true);
			this.tag = '{' + '{ek|' + reason + '}}';
			if(local_wgPageName.search(/^Szablon:/) != -1) {
				AjaxQuickDelete.tag = "<noinclude>" + AjaxQuickDelete.tag + "</noinclude>";
			}
			else {
				AjaxQuickDelete.tag += "\n";
			}

			this.closeDialog(sender);

			this.tasks = []; //clean current tasks
			this.addTask('showProgress');
			this.addTask('getEditToken');
			this.addTask('prependTemplate');
			this.addTask('reloadPage');
			this.nextTask();
		};

		/**
		 * Get an edit token.
		 *
		 * Only for speedy deletion. Reporting for discussion obtains the token
		 *   while performing other tasks.
		 */
		this.getEditToken = function () {
			var query = {
				formatversion: 2,
				curtimestamp: true,
				action: 'query',
				meta: 'tokens',
				prop: 'revisions',
				rvprop: 'timestamp',
				titles: local_wgPageName
			};
			this.doAPICall(query, 'getEditTokenCB');
		};

		/**
		 * Callback function of getEditToken(), called when the API call has
		 *   been completed.
		 *
		 * @param result Result returned by API
		 */
		this.getEditTokenCB = function (result) {
			this.edittoken = result.query.tokens.csrftoken;
			this.starttimestamp = result.curtimestamp;
			this.timestamp = result.query.pages[0].revisions[0].timestamp;
			this.updateProgress(this.i18n.preparingToEdit.replace('%COUNT%', ''));
			this.nextTask();
		};

		/**
		* Edit the current page (local_wgPageName) and prepend the specified tag.
		*
		* Assumes that the page hasn't been tagged yet; if it has, a duplicate tag
		*   will be added.
		*/
		this.prependTemplate = function () {
			var page = {};
			page.title = local_wgPageName;
			page.text = this.tag;
			page.editType = 'prependtext';
			if(window.AjaxDeleteWatchReportedPage) page.watchlist = 'watch';

			this.updateProgress(this.i18n.addingAnyTemplate);
			this.savePage(page, this.img_summary, 'nextTask');
		};

		/**
		* Create a deletion request subpage
		*
		* If the page already exists, the new request will be appended.
		*
		* The request page will be watchlisted if window.AjaxDeleteWatchRequest
		*   is not set to true.
		*/
		this.createRequestSubpage = function () {
			this.templateAdded = true;  // we've got this far; if something fails, user can follow instructions on template to finish

			var page = {};
			page.title = this.requestPage;
			page.text = "\n=== [[:" + local_wgPageName + "]] ===\n: {{lnSdU|" + local_wgPageName + "}}\n";
			page.text += this.reason + " ~~" + "~~\n";
			if(!window.AjaxDeleteDontWatchRequests)
				page.watchlist = 'watch';
			else
				page.watchlist = "nochange";
			page.editType = 'appendtext';

			this.updateProgress(this.i18n.creatingNomination);

			this.savePage(page, "Nowa dyskusja nad usunięciem", 'nextTask');
		};

		/**
		* Transclude the nomination page onto the main deletion request page (n:pl:WN:SdU).
		*
		* The log page will never be watchlisted (unless the user is already watching it).
		*/
		this.listRequestSubpage = function () {
			this.main_request_page = this.requestPagePrefix; // + '/' + this.request_type.subpage;

			this.updateProgress(this.i18n.listingNomination);

			this.fetchPage(this.main_request_page, 'listRequestSubpageCB');
		};

		/**
		 * Callback function of listRequestSubpage() called when the page
		 *   text has been fetched.
		 *
		 * Processes the fetched text adding the new request after the text
		 *   specified in this.section_line and saving the page.
		 *
		 * @param result Result returned by API
		 */
		this.listRequestSubpageCB = function (result) {
			var text = result.query.pages[result.query.pageids[0]].revisions[0]['*'];
			var insert = "\n{{" + this.requestPage + "}}";

			var page = {};
			page.text = text.replace(this.section_line, this.section_line + insert);
			if(page.text != text)
				page.editType = 'text';
			else {
				//could not find the section line? simply append
				page.text = insert;
				page.editType = 'appendtext';
			}
			page.title = this.main_request_page;
			page.watchlist = 'nochange';

			this.savePage(page, "Dodano [[" + this.requestPage + "]]", 'nextTask');
		};

		/**
		* Notify authors of this page (everybody listed in this.uploaders array)
		*
		* Currently on pl.wiki it is only the first author of a page
		*   (or all uploaders of a file except we don't have files any more).
		*
		* TODO: follow talk page redirects (including {{softredirect}}) // Should we really append a commons template on some user page in another wiki? Probably wouldn't work.
		*/
		this.notifyUploaders = function () {
			this.talk_tag = "{{SdUinfo|" + local_wgPageName + '|' + this.requestPage + "|" + this.request_type.template_param + "}}";

			this.uploadersToNotify = 0;
			for(var user in this.uploaders) {
				if(typeof (this.uploaders[user]) == 'function') continue; //on IE wikibits adds indexOf method for arrays. skip it.
				var page = {};
				page.title = this.userTalkPrefix + user;
				page.text = "\n\n== [[:" + local_wgPageName + "]] ==\n" + this.talk_tag + "\n~~" + "~~\n";
				page.editType = 'appendtext';
				if(window.AjaxDeleteWatchUserTalk) page.watchlist = 'watch';
				this.savePage(page, this.talk_summary, 'uploaderNotified');

				this.updateProgress(this.i18n.notifyingUploader.replace('%USER%', user));

				this.uploadersToNotify++;
			}
			if(this.uploadersToNotify == 0) this.nextTask();
		};

		/**
		 * Callback function of notifyUploaders()
		 *
		 * If we've run out of people to notify, proceed to other tasks.
		 */
		this.uploaderNotified = function () {
			this.uploadersToNotify--;
			if(this.uploadersToNotify == 0) this.nextTask();
		};

		/**
		 * Notify all the wikiprojects the user's selected.
		 *
		 * The wikiprojects to be notified should be listed in this.selected_wikiprojects.
		 *
		 * Depending on the "type" of the wikiproject (see this.wikiprojects), the
		 *   notification may be inserted after a comment line or appended as a new
		 *   section.
		 */
		this.notifyWikiproject = function () {
			//! Na Wikinews nie ma wikiprojektów
			this.nextTask();
		};

		/**
		 * Callback function of notifyWikiproject() called when the
		 *   wikiproject page text has been fetched.
		 *
		 * Called only when the wikiproject type was "section". Searches the page
		 *   for a specified comment line and inserts the notification beneath it.
		 *
		 * As a fallback, simply appends the notification.
		 *
		 * @param result Result returned by API
		 */
		this.notifyWikiprojectCB = function (result) {
			//! Funkcja jest nieużywana
			var section_line = "<!-- Nowe zgłoszenia wstawiaj poniżej tej linii. Nie usuwaj tej linii -->";

			var text = result.query.pages[result.query.pageids[0]].revisions[0]['*'];
			var page = {};
			page.title = this.current_wikiproject.page;
			page.text = text.replace(section_line, section_line + '\n' + this.talk_tag);
			if(text != page.text)
				page.editType = 'text';
			else {
				//could not find the section line? simply append
				page.editType = 'appendtext';
				page.text = this.talk_tag;
			}
			this.savePage(page, this.talk_summary, 'notifyWikiproject');
		};

		/**
		 * Find the author of the page.
		 *
		 * In case of a deletion request for a file, compile a list of uploaders to
		 *    notify. Users who have only reverted the file to an earlier version
		 *    will not be notified.
		 *
		 * Behaviour of the gadget for files has not been tested on pl.wiki, though
		 *   there's a chance it will work.
		*/
		this.findCreator = function () {
			var query;
			if(mw.config.get('wgNamespaceNumber') === 6) {
				query = {
					formatversion: 2,
					curtimestamp: true,
					action: 'query',
					meta: 'tokens',
					prop: 'imageinfo|revisions',
					rvprop: 'content|timestamp',
					rvslots: 'main',
					iiprop: 'user|sha1|comment',
					iilimit: 50,
					titles: local_wgPageName
				};

			} else {
				query = {
					formatversion: 2,
					curtimestamp: true,
					action: 'query',
					meta: 'tokens',
					prop: 'revisions',
					rvprop: 'user|timestamp',
					rvlimit: 1,
					rvdir: 'newer',
					titles: local_wgPageName
				};
			}
			this.doAPICall(query, 'findCreatorCB');
		};

		/**
		 * Callback function of findCreator(). Compiles the list of authors
		 *   from the data returned by API.
		 *
		 * @param {} result Result returned by API.
		 */
		this.findCreatorCB = function (result) {
			this.edittoken = result.query.tokens.csrftoken;
			this.starttimestamp = result.curtimestamp;
			this.uploaders = [];

			//First handle non-file pages
			if(mw.config.get('wgNamespaceNumber') !== 6) {
				var pageCreator = result.query.pages[0].revisions[0].user;
				this.timestamp = result.query.pages[0].revisions[0].timestamp;

				//Don't notify IPs
				//API provides an "anon" property but in case of some old edits
				//a registered user may also be marked with it. Use a regex instead.
				if(pageCreator.match(/^((\d{1,3}\.){3}\d{1,3}|([\dA-F]{0,4}:){7}[\dA-F]{0,4})$/) === null)
					this.uploaders[pageCreator] = true;
			} else {
				var info = result.query.pages[0].imageinfo;
				var content = result.query.pages[0].revisions[0].slots.main.content;
				var seenHashes = [];

				for(var i = info.length - 1; i >= 0; i--) {  // iterate in reverse order
					if(info[i].sha1 && seenHashes[info[i].sha1]) continue;  // skip reverts

					// Now exclude bots which only reupload a new version:
					this.excludedBots = 'FlickreviewR, Rotatebot, Cropbot, Picasa Review Bot';
					if(this.excludedBots.indexOf(info[i].user) != -1) continue;

					// Now exclude users with a {{nobots}} template as well as the user itself:
					this.excludedUsers = 'Arch dude, Avicennasis, BetacommandBot, Blood Red Sandman, Blurpeace, Captmondo, Coffee, Connormah, Denis Barthel, Editor at Large, Elvey, File Upload Bot (Kaldari), Grillo, Guandalug, IngerAlHaosului, Iune, Kbh3rd, Load, LobStoR, Megapixie, Michiel1972, Noodle snacks, OhanaUnited, Peteforsyth, Qwerty0, RenéV, Robinsonsmay, Rocket000, Sarkana, Shereth, Shoy, Sicherlich, Stepshep, The Earwig, V85, William S. Saturn, WJBscribe, ' + mw.config.get('wgUserName');
					if(this.excludedUsers.indexOf(info[i].user) != -1) continue;

					// Handle some special cases, most of the code by [[User:Lupo]]
					if(info[i].user == 'File Upload Bot (Magnus Manske)') {
						// CommonsHelper
						match = /transferred to Commons by \[\[User:([^\]\|]*)(\|([^\]]*))?\]\] using/.exec(info[i].comment);

						// geograph_org2commons, regex accounts for typo ("transferd") and it's possible future correction
						if(!match) match = /geograph.org.uk\]; transferr?e?d by \[\[User:([^\]\|]*)(\|([^\]]*))?\]\] using/.exec(info[i].comment);

						// flickr2commons
						if(!match) match = /\* Uploaded by \[\[User:([^\]\|]*)(\|([^\]]*))?\]\]/.exec(content);

						if(match) match = match[1];
						// Really necessary?
						match = this.fixDoubleEncoding(match);
					} else if(info[i].user == 'FlickrLickr') {
						match = /\n\|reviewer=\s*(.*)\n/.exec(content);
						if(match) match = match[1];
					} else if(info[i].user == 'Flickr upload bot') {
						// Check for the bot's upload template
						match = /\{\{User:Flickr upload bot\/upload(\|[^\|\}]*)?\|reviewer=([^\}]*)\}\}/.exec(content);
						if(match) match = match[2];
					} else {
						// No special case applies, just continue;
						this.uploaders[info[i].user] = true;
						continue;
					}
					if(match) {
						this.uploaders[match] = true;
					}
				}
			}

			// FIXME: How do we get the length of an associative array?
			this.updateProgress(this.i18n.preparingToEdit.replace('%COUNT%', ''));
			this.nextTask();
		};

		/**
		 * Now query the redirect status of all user talk pages, and, if
		 *   necessary, replace them in this.uploaders.
		 *
		 * Otherwise we would appendText to redirected pages, thus breaking them..
		 */
		this.resolveRedirects = function () {
			var pages = [];
			for(var user in this.uploaders) {
				if(typeof (this.uploaders[user]) == 'function') continue; //on IE wikibits adds indexOf method for arrays. skip it.
				pages.push(this.userTalkPrefix + user);
			}
			var query = {
				action: 'query',
				redirects: '',
				titles: pages.join('|')
			};
			this.doAPICall(query, 'resolveRedirectsCB');
		};

		/**
		 * Callback function of resolveRedirects()
		 *
		 * @param {} result Result returned by API
		 */
		this.resolveRedirectsCB = function (result) {
			if(result.query && result.query.redirects) {
				for(var i in result.query.redirects) {
					redirect = result.query.redirects[i];
					delete this.uploaders[redirect['from'].replace(this.userTalkPrefix, '')];

					this.uploaders[redirect['to'].replace(this.userTalkPrefix, '')] = true;
				}
			}
			this.nextTask();
		};

		/**
		 * Submit an edited page.
		 *
		 * @param {} page An object containing following fields:
		 *   - text - a new text of the page
		 *   - title - the title of the page to be edited.
		 *   - editType - whether the new text should replace the old one ('text'),
		 *       be appended ('appendtext'), prepended ('prependtext') or whatever
		 *       is allowed by API.
		 *   - watchlist - whether the page should be watched or not. Accepted values
		 *       as specified by API: "watch", "unwatch", "preferences", "nochange".
		 *       "preferences" is used as default when nothing else is specified.
		 * @param {string} summary Edit summary
		 * @param {string} callback A string specifying the method to be called when the query
		 *   is completed.
		 */
		this.savePage = function (page, summary, callback) {
			var edit = {
				action: 'edit',
				summary: summary,
				watchlist: (page.watchlist || 'preferences'),
				title: page.title,
				token: this.edittoken
			};

			edit[page.editType] = page.text;
			this.doAPICall(edit, callback);
		};

		/**
		* Get the page text.
		*
		* @param {string} page The title of the page to be fetched.
		* @param {string} callback A string specifying the method to be called when the
		*   query is completed.
		*/
		this.fetchPage = function (page, callback) {
			var query = {
				action: 'query',
				prop: 'revisions',
				rvprop: 'content',
				titles: page,
				indexpageids: '1'
			};
			this.doAPICall(query, callback);
		};

		/**
		 * Does a MediaWiki API request and passes the result to the supplied callback (method name).
		 *
		 * Uses POST requests for everything for simplicity.
		 *
		 * TODO: better error handling
		 *
		 * @param {object} params Query parameters.
		 * @param {string} callback A string specifying the method to be called when the
		 *   query is completed.
		 */
		this.doAPICall = function (params, callback) {
			var o = this;

			params.format = 'json';
			$.ajax({
				url: this.apiURL,
				cache: false,
				dataType: 'json',
				data: params,
				type: 'POST',
				success: function (result, status, x) {
					if(!result) return o.fail("Receive empty API response:\n" + x.responseText);

					// In case we get the mysterious 231 unknown error, just try again
					if(result.error && result.error.info.indexOf('231') != -1) return setTimeout(function () { o.doAPICall(params, callback); }, 500);
					if(result.error) return o.fail("API request failed (" + result.error.code + "): " + result.error.info);
					if(result.edit && result.edit.spamblacklist) return o.fail("The edit failed because " + result.edit.spamblacklist + " is on the Spam Blacklist");
					try { o[callback](result); } catch(e) { return o.fail(e); }
				},
				error: function (x, status, error) {
					return o.fail("API request returned code " + x.status + " " + status + "Error code is " + error);
				}
			});
		};

		/**
		* Simple task queue.
		*
		* addTask() adds a new task to the queue, nextTask() executes
		* the next scheduled task.  Tasks are specified as method names to call.
		*/
		this.tasks = [],

			/**
			 * current task, for error reporting
			 */
			this.currentTask = '',

			/**
			 * Adds a new task to the queue.
			 *
			 * @param {string} task String containting the name of a method to be added to the
			 *   queue.
			 */
			this.addTask = function (task) {
				this.tasks.push(task);
			};

		/**
		 * Executes the next scheduled task
		 */
		this.nextTask = function () {
			var task = this.currentTask = this.tasks.shift();
			try { this[task](); } catch(e) { this.fail(e); }
		};

		/**
		 * Once we're all done, reload the page.
		 */
		this.reloadPage = function () {
			var title = encodeURIComponent(this.destination || local_wgPageName).
				replace(/\%3A/g, ':').replace(/\%20/g, '_').
				replace(/\(/g, '%28').replace(/\)/g, '%29').
				replace(/\%2F/g, '/');

			location.href = mw.config.get('wgServer') + mw.config.get('wgArticlePath').replace("$1", title);
		};

		/**
		* Double encoding fixer by Lupo. This is necessary for some older uploads of Magnus' bot.
		*/
		this.fixDoubleEncoding = function (match) {
			if(!match) return match;
			var utf8 = /[u00C2-u00F4][u0080-u00BF][u0080-u00BF]?[u0080-u00BF]?/g;
			if(!utf8.test(match)) return match;
			// Looks like we have a double encoding. At least it contains character
			// sequences that might be legal UTF-8 encodings. Translate them into %-
			// syntax and try to decode again.
			var temp = "",
				curr = 0,
				m,
				hex_digit = "0123456789ABCDEF";
			var str = match.replace(/%/g, '%25');
			utf8.lastIndex = 0;
			// Reset regexp to beginning of string
			try {
				while((m = utf8.exec(str)) != null) {
					temp += str.substring(curr, m.index);
					m = m[0];
					for(var i = 0; i < m.length; i++) {
						temp += '%'
							+ hex_digit.charAt(m.charCodeAt(i) / 16)
							+ hex_digit.charAt(m.charCodeAt(i) % 16);
					}
					curr = utf8.lastIndex;
				}
				if(curr < str.length) temp += str.substring(curr);
				temp = decodeURIComponent(temp);
				return temp;
			} catch(e) {
			}
			return match;
		};

		/**
		 * Preprocess the reason for deletion before submitting.
		 *
		 * Trims whitespaces from the begining and the end and removes a signature.
		 *
		 * @param {string} uncleanReason The reason as input by the user.
		 * @returns {string} Cleaned reason.
		 */
		this.cleanReason = function (uncleanReason) {
			// trim whitespace
			uncleanReason = uncleanReason.replace(/^\s*(.+)\s*$/, '$1');
			// remove signature
			uncleanReason = uncleanReason.replace(/(.+)(--)?~{3,5}$/, '$1');
			return uncleanReason;
		};

		/**
		* Displays a dialogue informing the user what the script is doing on their behalf.
		*
		* @see updateProgress()
		*/
		this.showProgress = function () {
			document.body.style.cursor = 'wait';

			$progress = $('<div></div>')
				.html('<div id="feedbackContainer">' + this.i18n.preparingToEdit + '</div>')
				.dialog({
					width: 450,
					height: 90,
					minHeight: 90,
					modal: true,
					resizable: false,
					draggable: false,
					closeOnEscape: false,
					dialogClass: "ajaxDeleteFeedback"
				});
			$('.ui-dialog-titlebar').hide();

			this.nextTask();

		};

		/**
		 * Updates the message in the progress dialogue window.
		 *
		 * @param {string} message A new message.
		 *
		 * @see showProgress()
		 */
		this.updateProgress = function (message) {
			$('#feedbackContainer').html(message);
		};

		/**
		 * HTML select element listing available wikiprojects
		 */
		this.wikiproject_select = null;

		/**
		* Display the main dialogue window for the user to insert
		*   a deletion reason etc.
		*/
		this.prompt = function () {
			//question paragraph
			var $question_paragraph = $('<p></p>').append('Wyjaśnij dokładnie, dlaczego ta strona nie nadaje się do Wikinews:');

			//textarea paragraph
			//workaround for Opera - the textarea must be inserted to a visible element
			var $textarea_paragraph = $('<p id="ajax-delete-textarea-paragraph"></p>');
			//var $textarea_paragraph = $('<p></p>').append('<textarea id="AjaxQuestion" style="width: 98%;" rows="4" value=""></textarea>');

			//page type paragraph
			var $page_type_paragraph = $('<select id="ajax-delete-request-type"></select>').css('vertical-align', 'middle');
			$page_type_paragraph.append('<option value="none">wybierz pod dyskusję</option>');
			for(var i in this.request_types) {
				if(typeof (this.request_types[i]) == 'function') continue; //on IE wikibits adds indexOf method for arrays. skip it.
				$('<option value="' + i + '">' + this.request_types[i].label + '</option>')
					.appendTo($page_type_paragraph);
			}
			//! Usunięta funkcja do zmiany stanu przycisku w zależności od rodzaju zgłoszenia
			$page_type_paragraph = $('<p></p>').html('Rodzaj zgłoszenia: ').append($page_type_paragraph);

			//wikiproject paragraph
			this.wikiproject_select = $('<select class="ajax-delete-wikiproject"></select>').css('vertical-align', 'middle');
			this.wikiproject_select.append('<option value="none">żaden</option>');
			for(var i in this.wikiprojects) {
				if(typeof (this.wikiprojects[i]) == 'function') continue; //on IE wikibits adds indexOf method for arrays. skip it.
				$('<option value="' + i + '">' + this.wikiprojects[i].label + '</option>').appendTo(this.wikiproject_select);
			}

			var $wikiproject_paragraph = $('<span id="ajax-delete-wikiproject-container"></span>').append(this.wikiproject_select.clone());

			var $wikiproject_addWikiproject = $('<a href="#">(+)</a>').click(function (e) {
				e.preventDefault();
				AjaxQuickDelete.addWikiproject();
			});
			$wikiproject_paragraph = $('<p></p>').append('Jakie wikiprojekty powiadomić: ').append($wikiproject_paragraph)
				.append($wikiproject_addWikiproject);

			//rules paragraph
			var rules_path = mw.config.get('wgServer') + mw.config.get('wgArticlePath').replace('$1', 'Wikinews:Zasady_ekspresowego_kasowania');
			var $rules_paragraph = $('<p></p>').html('<small>Jeżeli strona spełnia <a href="' + rules_path + '" target="_blank" style="font-weight:bold;">zasady ekspresowego kasowania</a>, zamiast rozpoczynać nad nią dyskusję możesz zgłosić ją do <b>ekspresowego kasowania</b>. Aby to zrobić, w polu powyżej podaj powód, a następnie kliknij przycisk „Ekspresowe kasowanie”. Pozostałe pola zostaną wtedy zignorowane.</small>');

			//build the dialog
			var $dialog = $('<div></div>').append($question_paragraph).append($textarea_paragraph).
				append($rules_paragraph);

			//main buttons
			var buttons = [
				{
					id: "poddaj-pod-dyskusje",
					text: "Poddaj pod dyskusję",
					click: function () {
						// if($('#ajax-delete-request-type').val() == "none") {
						// 	alert("Proszę wybrać rodzaj zgłoszenia");
						// 	return;
						// }

						//get the reason
						//response = AjaxQuickDelete.getAjaxQuestionAnswer();
						//AjaxQuickDelete.reason = response;
						AjaxQuickDelete.reason = AjaxQuickDelete.getAjaxQuestionAnswer();

						//AjaxQuickDelete.tag = AjaxQuickDelete.tag.replace('%PARAMETER%', response);
						//AjaxQuickDelete.img_summary = AjaxQuickDelete.img_summary.replace('%PARAMETER%', response);
						//AjaxQuickDelete.img_summary = AjaxQuickDelete.img_summary.replace('%PARAMETER-LINKED%', '[[:' + response + ']]');

						//get the request type
						var i = $('#ajax-delete-request-type').val();
						//! Na sztywno 0
						AjaxQuickDelete.request_type = {
							template_param: 'artykuł',
							subpage: 'artykuły',
							label: 'artykuł (niebędący biografią)',
							img_summary: 'Zgłoszenie do usunięcia',
							talk_summary: "Strona [[:" + local_wgPageName + "]] została zgłoszona do usunięcia"
						};
						//! AjaxQuickDelete.request_types[i];
						//and summaries
						AjaxQuickDelete.img_summary = AjaxQuickDelete.request_type.img_summary;
						AjaxQuickDelete.talk_summary = AjaxQuickDelete.request_type.talk_summary;

						//get the wikiprojects
						//! Nie ma wikiprojektów
						// var $projsel = $('.ajax-delete-wikiproject');
						AjaxQuickDelete.selected_wikiprojects = [];
						// $projsel.each(function (index) {
						// 	var val = $(this).val();
						// 	if(val != 'none')
						// 		AjaxQuickDelete.selected_wikiprojects.push(AjaxQuickDelete.wikiprojects[val]);
						// });

						AjaxQuickDelete.requestPageTitle = AjaxQuickDelete.formatDate("YYYY:MM:DD:") + local_wgPageName;
						AjaxQuickDelete.requestPage = AjaxQuickDelete.requestPagePrefix + /* '/' + AjaxQuickDelete.request_type.subpage +*/ '/' + AjaxQuickDelete.requestPageTitle;
						AjaxQuickDelete.tag = "{{SdU|" + AjaxQuickDelete.request_type.template_param + "|podstrona=" + AjaxQuickDelete.requestPageTitle + "}}\n";

						if(local_wgPageName.search(/^Szablon:/) != -1) {
							AjaxQuickDelete.tag = "<noinclude>" + AjaxQuickDelete.tag + "</noinclude>";
						}

						AjaxQuickDelete.closeDialog(this);
						AjaxQuickDelete.nextTask();
					}
				},
				{
					text: "Ekspresowe kasowanie",
					click: function () {
						AjaxQuickDelete.speedyDeletionClicked();
					}
				},
				{
					text: "Anuluj",
					click: function () {
						$(this).dialog("close");
					}
				},
			];

			$dialog.dialog({
				width: 600,
				modal: true,
				title: 'Zgłaszanie strony do usunięcia',
				draggable: true,
				dialogClass: "wikiEditor-toolbar-dialog",
				close: function () { $(this).dialog("destroy"); $(this).remove(); },
				buttons: buttons
			});

			//insert the main textarea
			$('#ajax-delete-textarea-paragraph').html('<textarea id="AjaxQuestion" style="width: 98%;" rows="4" value=""></textarea>');

			/*var submitButton = $('.ui-dialog-buttonpane button:first');
			$('#AjaxQuestion').keyup(function(event) {
			  if ($(this).val().length < 4 && noEmpty) {
				  submitButton.addClass('ui-state-disabled');
			  } else {
				  submitButton.removeClass('ui-state-disabled');
			  }
			  if (event.keyCode == '13') submitButton.click();
			});*/
			$('#AjaxQuestion').keyup();
			$('#AjaxQuestion').focus();
		};

		/**
		* Get the text the user's typed in the dialog
		*/
		this.getAjaxQuestionAnswer = function () {
			var response = $('#AjaxQuestion').val();
			//alert($('#AjaxQuestion').html());
			response = AjaxQuickDelete.cleanReason(response);
			return response;
		};

		/**
		 * Close a dialogue window.
		 *
		 * @param {} object The dialogue to be closed.
		 */
		this.closeDialog = function (object) {
			$('#AjaxQuestion').remove();
			$(object).dialog("destroy");
			$(object).remove();
		};

		/**
		 * Crude error handler. Just throws an alert at the user and (if we managed to
		 *   add the deletion template) reloads the page.
		 *
		 * @param {} err Error
		 */
		this.fail = function (err) {
			document.body.style.cursor = 'default';
			var msg = this.i18n.taskFailure[this.currentTask] || this.i18n.genericFailure;
			var fix = (this.templateAdded ? this.i18n.completeRequestByHand : this.i18n.addTemplateByHand);

			$('#feedbackContainer').html(msg + " " + fix + "<br>" + this.i18n.errorDetails + "<br>" + err + '<br><a href="' + mw.util.getUrl('MediaWiki_talk:AjaxQuickDelete.js') + '">' + this.i18n.errorReport + "</a>");
			$('#feedbackContainer').addClass('ajaxDeleteError');

			// Allow some time to read the message
			if(this.templateAdded) {
				var that = this;
				setTimeout(function () {
					that.reloadPage();
				}, 5000);
			}
		};

		/**
		* Very simple date formatter.
		*
		* Replaces the substrings "YYYY", "MM" and "DD" in a
		* given string with the UTC year, month and day numbers respectively.  Also
		* replaces "MON" with the English full month name and "DAY" with the unpadded day.
		*
		* @param {string} fmt Date format string
		* @param {Date} date Date to be formatted
		*
		* @returns {string} Formatted date.
		*/
		this.formatDate = function (fmt, date) {
			var pad0 = function (s) { s = "" + s; return (s.length > 1 ? s : "0" + s); };  // zero-pad to two digits
			if(!date) date = this.startDate;
			fmt = fmt.replace(/YYYY/g, date.getUTCFullYear());
			fmt = fmt.replace(/MM/g, pad0(date.getUTCMonth() + 1));
			fmt = fmt.replace(/DD/g, pad0(date.getUTCDate()));
			fmt = fmt.replace(/MON/g, this.months[date.getUTCMonth()]);
			fmt = fmt.replace(/DAY/g, date.getUTCDate());
			return fmt;
		};
		/**
		 * Month names
		 */
		this.months = ['styczeń', 'luty', 'marzec', 'kwiecień', 'maj', 'czerwiec', 'lipiec', 'sierpień', 'wrzesień', 'październik', 'listopad', 'grudzień'],

			/**
			 * Wikiprojects which should be notified. Selected by the user.
			 */
			this.selected_wikiprojects = [],

			/**
			 * DR subpage prefix
			 */
			this.requestPagePrefix = "Wikinews:Strony_do_usunięcia",
			/**
			 * user talk page prefix
			 */
			this.userTalkPrefix = mw.config.get('wgFormattedNamespaces')[3] + ":",
			/**
			 * MediaWiki API script URL
			 */
			this.apiURL = mw.config.get('wgServer') + mw.config.get('wgScriptPath') + "/api.php",

			/**
			 * Translatable strings
			 */
			this.i18n = {
				toolboxLinkDelete: "Zgłoś do usunięcia",

				// GUI reason prompt form
				reasonForDeletion: "Czemu twoim zdaniem ta strona powinna zostać usunięta?",
				reasonForDiscussion: "Why does this category need discussion?",
				submitButtonLabel: "Zgłoś",
				cancelButtonLabel: "Anuluj",

				// GUI progress messages
				preparingToEdit: "Szykuję się do edytowania... ",
				creatingNomination: "Tworzę stronę zgłoszenia... ",
				listingNomination: "Dodaję zgłoszenie na stronę SdU... ",
				addingAnyTemplate: "Dodaję szablon na zgłaszanej stronie...",
				notifyingUploader: "Powiadamiam użytkownika %USER%... ",

				// Extended version
				toolboxLinkSource: "No source",
				toolboxLinkLicense: "No license",
				toolboxLinkPermission: "No permission",
				toolboxLinkCopyvio: "Report copyright violation",
				reasonForCopyvio: "Why is this file a copyright violation?",

				// For moving files
				renameDone: "Removing template; rename actioned",
				removingTemplate: "Removing rename template",
				notAllowed: "You do not have the neccessary rights to move files",
				reasonForMove: "Why do you want to move this file?",
				moveDestination: "What should be the new file name?",
				movingFile: "Moving file",
				replacingUsage: "Ordering CommonsDelinker to replace all usage",
				declineMove: "Why do you want to decline the request?",
				dropdownMove: "Move & Replace",

				// Errors
				genericFailure: "Wysŧąpił błąd podczas zgłaszania do usunięcia.",
				taskFailure: {
					getEditToken: "Wystąpił błąd podczas uzyskiwania tokenu edycji.",
					listUploaders: "Wystąpił błąd podczas wyszukiwania autora artykułu.",
					loadPages: "Wystąpił błąd podczas przygotowywania się do zgłoszenia strony.",
					prependDeletionTemplate: "Wystąpił błąd podczas dodawania szablonu {{SdU}} na zgłaszanej stronie.",
					createRequestSubpage: "Wystąpił błąd podczas tworzenia strony zgłoszenia.",
					listRequestSubpage: "Wystąpił błąd podczas dodawania zgłoszenia na stronę Poczekalni.",
					notifyUploaders: "Wystąpił błąd podczas powiadamiania autora.",
					notifyWikiproject: "Wystąpił błąd podczas powiadamiania wikiprojektu."
				},
				addTemplateByHand: "To nominate this " + mw.config.get('wgCanonicalNamespace').toLowerCase() + " for deletion, please edit the page to add the {{del" + "ete}} template and follow the instructions shown on it.",
				completeRequestByHand: "Please follow the instructions on the deletion notice to complete the request.",
				errorDetails: "Szczegółowy opis błędu:",
				errorReport: "Zgłoś nieprawidłowe działanie"
			};

		/**
		 * Types of deletion requests
		 *
		 * Objects containing following fields:
		 * - template_param - parameter which should be inserted into a deletion
		 *     template ({SdU})
		 * - subpage - subpage of the deletion request page (WN:SdU)
		 * - label - the text displayed in the dialogue
		 */
		this.request_types = [];

		/**
		 * List of wikiproject which will appear on the list of wikiprojects to be notified.
		 *
		 * Objects containing following fields:
		 * label - text which will appear in the dropdown menu
		 * page - location of the wikiproject. If type is 'talk', page should point to the
		 *        wikiproject talk page
		 * type - 'section' or 'talk'
		 *        - 'section' - the template will be put on the wikiproject main page, after a line
		 *                    "<!-- Nowe zgłoszenia wstawiaj poniżej tej linii. Nie usuwaj tej linii -->" (without quotes)
		 *        - 'talk' - the template will be placed in a new section on the wikiproject talk page.
		 */
		this.wikiprojects = [];
		this.wikiprojects_tech = [];
	};

	var AjaxQuickDelete = new AjaxQuickDeleteClass();

	//onload hook
	mediaWiki.loader.using(['jquery.ui', 'mediawiki.util'], function () {
		jQuery(document).ready(function () {
			AjaxQuickDelete.install();
		});
	});

} // end if (guard)
// </nowiki>