MediaWiki:Gadget-Editor.js

// 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 = $('').css("background-color", "#FFDDDD") .css("margin", "0px -10px -10px -10px") .css("padding", "10px")[0]; withPresenceShowing(true, function(presence) {				presence.appendChild(errorlog);			}); }		errorlog.appendChild($('').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 = $(' ').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($(' ').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 = $(' ').css('text-align', 'center') .append($('Page Editing '));

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

contents.append($(' ').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 = $('Saving:' + summary + '...'); withPresenceShowing(false, function(presence) {				if (!$savelog) {					$savelog = $('').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 + "(Assisted)",				notminor: true			}).then(function(res) {				if (res == null)					return thiz.error("An error occurred while saving.");

try { saveLi.append(						$(' ')						.append($("Saved"))						.append($('').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($(' ').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 = $(' ')[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( $(' ').css("color", "red") .append($(' ').attr('src', 'http://upload.wikimedia.org/wikipedia/commons/4/4e/MW-Icon-AlertMark.png')) .append(msg) .append($(' '))[0]);						adder.elements[name].style.border = "solid #CC0000 2px";						return false					});

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

var loading = $(' Loading... ')[0]; status.appendChild(loading);

adder.onsubmit(values, function(text, callback) {				//text = "" + text + " ";				//text = " " + text + " ";				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($(' Could not connect to the server ').css("color", "red")[0]); });			});		} catch (e) { status.innerHTML = "ERROR:" + e.description; return false; }		return false; } };