MediaWiki:Gadget-Editor.js

From Wiktionary, the free dictionary
Jump to navigation Jump to search

Note – after saving, you may have to bypass your browser’s cache to see the changes.

  • Mozilla / Firefox / Safari: hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (Command-R on a Macintosh);
  • Konqueror and Chrome: click Reload or press F5;
  • Opera: clear the cache in Tools → Preferences;
  • Internet Explorer: hold Ctrl while clicking Refresh, or press Ctrl-F5.

This actually is not a standalone gadget but rather a library.

This piece of code used to be in the same source as TranslationAdder. It was taken out of it because

  1. theoretically one could have translation adder turned off
  2. separation of concerns.

// This page consists of Editor and AdderWrapper
// Author: Conrad Irwin
/*jshint maxerr:1048576, strict:true, undef:true, latedef:true, es5:true */
/*global mw, jQuery, importScript, importScriptURI, $ */
window.PageEditor = function(title) {
	this.CheckOutForEdit = function() {
		return new mw.Api().get({
				action: 'query',
				prop: 'revisions',
				rvprop: ['ids', 'content', 'timestamp'],
				titles: String(title),
				formatversion: '2',
				curtimestamp: true
			})
			.then(function(data) {
				var page, revision;
				if (!data.query || !data.query.pages) {
					return $.Deferred().reject('unknown');
				}
				page = data.query.pages[0];
				if (!page || page.missing) {
					return $.Deferred().reject('nocreate-missing');
				}
				revision = page.revisions[0];
				this.baserevid = revision.revid;
				this.basetimestamp = revision.timestamp;
				this.curtimestamp = data.curtimestamp;
				return revision.content;
			});
	}
	this.Save = function(newWikitext, params) {
		var editParams = typeof params === 'object' ? params : {
			text: String(params)
		};
		return new mw.Api().postWithEditToken($.extend({
			action: 'edit',
			title: title,
			formatversion: '2',
			text: newWikitext,

			// Protect against errors and conflicts
			assert: mw.user.isAnon() ? undefined : 'user',
			baserevid: this.baserevid,
			basetimestamp: this.basetimestamp,
			starttimestamp: this.curtimestamp,
			nocreate: true
		}, editParams));
	}
}

/**
 * A generic page editor for the current page.
 *
 * This is a singleton and it displays a small interface in the top left after
 * the first edit has been registered.
 *
 * @public
 * this.page
 * this.addEdit
 * this.error
 *
 */

window.Editor = function() {
	//Singleton
	if (arguments.callee.instance)
		return arguments.callee.instance;
	else
		arguments.callee.instance = this;

	this.page = new PageEditor(mw.config.get('wgPageName'));

	// get the current text of the article and call the callback with it
	// NOTE: This function also acts as a loose non-re-entrant lock to protect currentText.
	this.withCurrentText = function(callback) {
		if (callbacks.length == 0) {
			callbacks = [callback];
			for (var i = 0; i < callbacks.length; i++) {
				callbacks[i](currentText);
			}
			return callbacks = [];
		}

		if (callbacks.length > 0) {
			return callbacks.push(callback);
		}


		callbacks = [callback];
		thiz.page.CheckOutForEdit().then(function(wikitext) {
			if (wikitext === null)
				return thiz.error("Could not connect to server");

			currentText = originalText = wikitext;

			for (var i = 0; i < callbacks.length; i++) {
				callbacks[i](currentText);
			}
			callbacks = [];
		});
	}
	// A decorator for withCurrentText
	function performSequentially(f) {
		return (function() {
			var the_arguments = arguments;
			thiz.withCurrentText(function() {
				f.apply(thiz, the_arguments);
			});
		});
	}

	// add an edit to the editstack
	function addEdit(edit, node, fromRedo) {
		withPresenceShowing(false, function() {
			if (node) {
				nodestack.push(node);
				node.style.cssText = "border: 2px #00FF00 dashed;"
			}

			if (!fromRedo)
				redostack = [];

			var ntext = false;
			try {
				ntext = edit.edit(currentText);

				if (ntext && ntext != currentText) {
					edit.redo();
					currentText = ntext;
				} else
					return false;
			} catch (e) {
				// TODO Uncaught TypeError: Object [object Window] has no method 'error'
				// I may have just fixed this by changing "this" below to "thiz" ...
				thiz.error("ERROR:" + e);
			}

			editstack.push(edit);
		});
	}
	this.addEdit = performSequentially(addEdit);

	// display an error to the user
	this.error = function(message) {
		console.trace(message);
		if (!errorlog) {
			errorlog = $('<ul>').css("background-color", "#FFDDDD")
				.css("margin", "0px -10px -10px -10px")
				.css("padding", "10px")[0];
			withPresenceShowing(true, function(presence) {
				presence.appendChild(errorlog);
			});
		}
		errorlog.appendChild($('<li>').text(message)[0]);
	}

	var thiz = this; // this is set incorrectly when private functions are used as callbacks.

	var editstack = []; // A list of the edits that have been applied to get currentText
	var redostack = []; // A list of the edits that have been recently undone.
	var nodestack = []; // A lst of nodes to which we have added highlighting
	var callbacks = {}; // A list of onload callbacks (initially .length == undefined)

	var originalText = ""; // What was the contents of the page before we fiddled?
	var currentText = ""; // What is the contents now?

	var errorlog; // The ul for sticking errors in.
	var $savelog; // The ul for save messages.

	//Move an edit from the editstack to the redostack 
	function undo() {
		if (editstack.length == 0)
			return false;
		var edit = editstack.pop();
		redostack.push(edit);
		edit.undo();

		var text = originalText;
		for (var i = 0; i < editstack.length; i++) {
			var ntext = false;
			try {
				ntext = editstack[i].edit(text);
			} catch (e) {
				thiz.error("ERROR:" + e);
			}
			if (ntext && ntext != text) {
				text = ntext;
			} else {
				editstack[i].undo();
				editstack = editstack.splice(0, i);
				break;
			}
		}
		currentText = text;
		return true;
	}
	this.undo = performSequentially(undo);

	//Move an edit from the redostack to the editstack
	function redo() {
		if (redostack.length == 0)
			return;
		var edit = redostack.pop();
		addEdit(edit, null, true);
	}
	this.redo = performSequentially(redo);

	function withPresenceShowing(broken, callback) {
		if (arguments.callee.presence) {
			arguments.callee.presence.style.display = "block";
			return callback(arguments.callee.presence);
		}

		var presence = $('<div>').css("position", "fixed")
			.css("top", "0px")
			.css("left", "0px")
			.css("background-color", "#00FF00")
			.css("z-index", "10")
			.css("padding", "30px")[0];

		window.setTimeout(function() {
			presence.style.backgroundColor = "#CCCCFF";
			presence.style.padding = "10px";
		}, 400);

		presence.appendChild($('<div>').css("position", "relative")
			.css("top", "0px")
			.css("left", "0px")
			.css("margin", "-10px")
			.css("color", "#0000FF")
			.css("cursor", "pointer")
			.on("click", performSequentially(close))
			.text("X")[0]);

		document.body.insertBefore(presence, document.body.firstChild);

		var contents = $('<p>').css('text-align', 'center')
			.append($('<b>Page Editing</b></br>'));

		if (!broken) {
			contents.append($('<button>').text("Save Changes")
				.attr('title', 'Save your changes [s]')
				.attr('accesskey', 's')
				.on("click", save));
			contents.append($('<br>'));
			contents.append($('<button>').text("Undo")
				.attr('title', 'Undo last change [z]')
				.attr('accesskey', 'z')
				.on("click", thiz.undo));

			contents.append($('<button>').text("Redo").on('click', thiz.redo));

			mw.loader.using('mediawiki.util').then(function() {
				contents.children().updateTooltipAccessKeys();
			});
		}
		presence.appendChild(contents[0]);

		arguments.callee.presence = presence;
		callback(presence);
	}

	// Remove the button
	function close() {
		while (undo())
		;

		withPresenceShowing(true, function(presence) {
			presence.style.display = "none";
			if (errorlog) {
				errorlog.parentNode.removeChild(errorlog);
				errorlog = false;
			}
		});
	}

	//Send the currentText back to the server to save.
	function save() {
		thiz.withCurrentText(function() {
			if (editstack.length == 0)
				return;

			var cleanup_callbacks = callbacks;
			callbacks = [];
			var sum = {};
			for (var i = 0; i < editstack.length; i++) {
				sum[editstack[i].summary] = true;
				if (editstack[i].after_save)
					cleanup_callbacks.push(editstack[i].after_save);
			}
			var summary = "";
			for (var name in sum) {
				summary += name + " ";
			}
			editstack = [];
			redostack = [];
			var saveLi = $('<li>Saving:' + summary + '...</li>');
			withPresenceShowing(false, function(presence) {
				if (!$savelog) {
					$savelog = $('<ul>').css("background-color", "#DDFFDD")
						.css("margin", "0px -10px -10px -10px")
						.css("padding", "10px");
					$(presence).append($savelog);
				}
				$savelog.append(saveLi);

				if (originalText == currentText)
					return thiz.error("No changes were made to the page.");

				else if (!currentText)
					return thiz.error("ERROR: page has become blank.");
			});

			originalText = currentText;
			var nst = []
			var node;
			while (node = nodestack.pop()) {
				nst.push(node);
			}
			thiz.page.Save(currentText, {
				summary: summary + "([[WT:EDIT|Assisted]])",
				notminor: true
			}).then(function(res) {
				if (res == null)
					return thiz.error("An error occurred while saving.");

				try {
					saveLi.append(
						$('<span>')
						.append($("<b>Saved</b>"))
						.append($('<a>').attr("href", mw.config.get('wgScript') +
								'?title=' + encodeURIComponent(mw.config.get('wgPageName')) +
								'&diff=' + encodeURIComponent(res.edit.newrevid) +
								'&oldid=' + encodeURIComponent(res.edit.oldrevid))
							.text("(Show changes)")));
				} catch (e) {
					if (res.error) {
						thiz.error("Not saved: " + String(res.error.info));
					} else {
						thiz.error($('<p>').text(String(e))[0]);
					}
				}

				for (var i = 0; i < nst.length; i++)
					nst[i].style.cssText = "background-color: #0F0;border: 2px #0F0 solid;";

				window.setTimeout(function() {
					var node;
					while (node = nst.pop())
						node.style.cssText = "";
				}, 400);

				// restore any callbacks that were waiting for currentText before we started
				for (var i = 0; i < cleanup_callbacks.length; i++)
					thiz.withCurrentText(cleanup_callbacks[i]);

			});
		});
	}
}



/**
 * A small amount of common code that can be usefully applied to adder forms.
 *
 * An adder is assumed to be an object that has:
 *
 * .fields  A object mapping field names to either validation functions used
 *          for text fields, or the word 'checkbox'
 *
 * .createForm  A function () that returns a newNode('form') to be added to the
 *              document (by appending to insertNode)
 *
 * .onsubmit  A function (values, register (wikitext, callback)) that accepts
 *            the validated set of values and processes them, the register
 *            function accepts wikitext and a continuation function to be
 *            called with the result of rendering it.
 *
 * Before onsubmit or any validation functions are called, but after running
 * createForm, a new property .elements will be added to the adder which is a
 * dictionary mapping field names to HTML input elements.
 *
 * @param {editor}  The current editor.
 * @param {adder}  The relevant adder.
 * @param {insertNode}  Where to insert this in the document.
 * @param {insertSibling} Where to insert this within insertNode.
 */
window.AdderWrapper = function(editor, adder, insertNode, insertSibling) {
	console.trace("In AdderWrapper v1.0");
	
	var form = adder.createForm()
	var status = $('<span>')[0];

	form.appendChild(status);
	if (insertSibling)
		insertNode.insertBefore(form, insertSibling);
	else
		insertNode.appendChild(form);

	adder.elements = {};

	//This is all because IE doesn't reliably allow form.elements['name']
	for (var i = 0; i < form.elements.length; i++) {
		adder.elements[form.elements[i].name] = form.elements[i];
	}

	form.onsubmit = function() {
		try {
			var submit = true;
			var values = {}

			status.innerHTML = "";
			for (var name in adder.fields) {
				if (adder.fields[name] == 'checkbox') {
					values[name] = adder.elements[name].checked ? name : false;
				} else {
					adder.elements[name].style.border = ''; // clear error styles
					values[name] = adder.fields[name](adder.elements[name].value || '', function(msg) {
						status.appendChild(
							$('<span>').css("color", "red")
							.append($('<img>').attr('src', 'http://upload.wikimedia.org/wikipedia/commons/4/4e/MW-Icon-AlertMark.png'))
							.append(msg)
							.append($('<br>'))[0]);
						adder.elements[name].style.border = "solid #CC0000 2px";
						return false
					});

					if (values[name] === false)
						submit = false;
				}
			}
			if (!submit)
				return false;

			var loading = $('<span>Loading...</span>')[0];
			status.appendChild(loading);

			adder.onsubmit(values, function(text, callback) {
				//text = "<p style='display:inline;'>" + text + "</p>";
				//text = "<div id='editorjs-temp'>" + text + "</div>";
				new mw.Api().parse(text, {
						title: mw.config.get('wgPageName'),
						pst: true, //pst makes subst work as expected
						disablelimitreport: true
					})
					.then(function(r) {
						var cleanedHtml = $.parseHTML(r)[0].children[0].innerHTML; //first child of .mw-parser-output
						callback(cleanedHtml);
						status.removeChild(loading);
					}).fail(function(r) {
						if (r) console.log("ERROR IN Editor.js:" + r);
						loading.appendChild($('<p>Could not connect to the server</p>').css("color", "red")[0]);
					});
			});
		} catch (e) {
			status.innerHTML = "ERROR:" + e.description;
			return false;
		}
		return false;
	}
};