MediaWiki:LAPI.js

/* Small JS library containing stuff I use often.

Author: User:Lupo, June 2009 License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)

Choose whichever license of these you like best :-)

Includes the following components: - Object enhancements (clone, merge) - String enhancements (trim, ...) - Array enhancements (JS 1.6) - Function enhancements (bind) - LAPI           Most basic DOM functions: $ (getElementById), make -  LAPI.Ajax     Ajax request implementation, tailored for MediaWiki/WMF sites -  LAPI.Browser  Browser detection (general) -  LAPI.DOM      DOM helpers, including a cross-browser DOM parser -  LAPI.WP       MediaWiki/WMF-specific DOM routines -  LAPI.Edit     Simple editor implementation with save, cancel, preview (for WMF sites) -  LAPI.Evt      Event handler routines (general) -  LAPI.Pos      Position calculations (general) // // Global: importScript (from wiki.js, for MediaWiki:AjaxSubmit.js) // Configuration: set this to the URL of your image server. The value is a string representation // of a regular expression. For instance, for Wikia, use "http://images\\d\\.wikia\\.nocookie\\.net". // Remember to double-escape the backslash. /* global importScript, LAPI, ajaxSubmit */ /* jshint unused:false, laxcomma:true, smarttabs:true, loopfunc:true, forin:false */ /* eslint-disable one-var, vars-on-top, camelcase, no-use-before-define, eqeqeq, no-bitwise */ if (window.LAPI_file_store === undefined) var LAPI_file_store = '(https?:)?//static\\.jojowiki\\.com/'; // Some basic routines, mainly enhancements of the String, Array, and Function objects. // Some taken from JavaScript 1.6, some own. /** Object enhancements ************/ // Note: adding these to the prototype may break other code that assumes that // {} has no properties at all. Object.clone = function (source, includeInherited) { if (!source) return null; var result = {}; for (var key in source) { if (includeInherited || source.hasOwnProperty(key)) result[key] = source[key]; }	return result; };

Object.merge = function (from, into, includeInherited) { if (!from) return into; for (var key in from) { if (includeInherited || from.hasOwnProperty(key)) into[key] = from[key]; }	return into; };

Object.mergeSome = function (from, into, includeInherited, predicate) { if (!from) return into; if (!predicate) return Object.merge(from, into, includeInherited); for (var key in from) { if ((includeInherited || from.hasOwnProperty(key)) && predicate(from, into, key)) into[key] = from[key]; }	return into; };

Object.mergeSet = function (from, into, includeInherited) { return Object.mergeSome(from, into, includeInherited, function (src, tgt, key) {		return src[key] !== null;	}); };

/** String enhancements (JavaScript 1.6) ************/

// Removes given characters from the beginning of the string. // If no characters are given, defaults to removing whitespace. if (!String.prototype.trimLeft) { String.prototype.trimLeft = function (chars) { if (!chars) return this.replace(/^\s\s*/, ''); return this.replace(new RegExp('^[' + chars.escapeRE + ']+'), ''); }; } String.prototype.trimFront = String.prototype.trimLeft; // Synonym

// Removes given characters from the end of the string. // If no characters are given, defaults to removing whitespace. if (!String.prototype.trimRight) { String.prototype.trimRight = function (chars) { if (!chars) return this.replace(/\s\s*$/, ''); return this.replace(new RegExp('[' + chars.escapeRE + ']+$'), ''); }; } String.prototype.trimEnd = String.prototype.trimRight; // Synonym

/** Further String enhancements ************/

// Returns true if the string begins with prefix. if (!String.prototype.startsWith) { String.prototype.startsWith = function (prefix) { return this.indexOf(prefix) === 0; }; }

// Returns true if the string ends in suffix if (!String.prototype.endsWith) { String.prototype.endsWith = function (suffix) { return this.lastIndexOf(suffix) + suffix.length === this.length; }; }

// Returns true if the string contains s. String.prototype.contains = function (s) { return this.indexOf(s) >= 0; };

// Replace all occurrences of a string pattern by replacement. String.prototype.replaceAll = function (pattern, replacement) { return this.split(pattern).join(replacement); };

// Escape all backslashes and single or double quotes such that the result can // be used in JavaScript inside quotes or double quotes. String.prototype.stringifyJS = function { return this.replace(/([\\'"]|%5C|%27|%22)/g, '\\$1') // ' // Fix syntax coloring		.replace(/\n/g, '\\n'); };

// Escape all RegExp special characters such that the result can be safely used // in a RegExp as a literal. String.prototype.escapeRE = function { return this.replace(/([\\{}|.?*+^$[\]])/g, '\\$1'); };

String.prototype.escapeXML = function (quot, apos) { var s = this.replace(/&/g, '&amp;') .replace(/\xa0/g, ' ') .replace(//g, '&gt;'); if (quot) s = s.replace(/"/g, '&quot;'); // " // Fix syntax coloring if (apos) s = s.replace(/'/g, '&apos;'); // ' // Fix syntax coloring return s; };

String.prototype.decodeXML = function { return this.replace(/&quot;/g, '"')		.replace(/&apos;/g, '\'')		.replace(/&gt;/g, '>')		.replace(/&lt;/g, '<')		.replace(/ /g, '\xa0')		.replace(/&amp;/g, '&'); };

String.prototype.capitalizeFirst = function { return this.substring(0, 1).toUpperCase + this.substring(1); };

String.prototype.lowercaseFirst = function { return this.substring(0, 1).toLowerCase + this.substring(1); };

// This is actually a function on URLs, but since URLs typically are strings in // JavaScript, let's include this one here, too. String.prototype.getParamValue = function (param) { var re = new RegExp('[&?]' + param.escapeRE + '=([^&#]*)'), m = re.exec(this); if (m && m.length >= 2) return decodeURIComponent(m[1]); return null; };

String.getParamValue = function (param, url) { url = url || document.location.href; try { return url.getParamValue(param); } catch (e) { return null; } };

/** Function enhancements (JavaScript 1.8.5.) ************/

if (!Function.prototype.bind) { // Return a function that calls the function with 'this' bound to 'thisObject' Function.prototype.bind = function (thisObject) { var f = this, obj = thisObject, slice = Array.prototype.slice, prefixedArgs = slice.call(arguments, 1); return function { return f.apply(obj, prefixedArgs.concat(slice.call(arguments))); };	}; }

/** Array enhancements (JavaScript 1.6) ************/

// Note that contrary to JS 1.6, we treat the thisObject as optional. // Don't add to the prototype, that would break for (var key in array) loops!

// Returns a new array containing only those elements for which predicate // is true. if (!Array.filter) { Array.filter = function (target, predicate, thisObject) { if (target === null) return null; if (typeof target.filter === 'function') return target.filter(predicate, thisObject); if (typeof predicate !== 'function') throw new Error('Array.filter: predicate must be a function'); var l = target.length, result = []; if (thisObject) predicate = predicate.bind(thisObject); for (var i = 0; i < l; i++) { if (i in target) { var curr = target[i]; if (predicate(curr, i, target)) result[result.length] = curr; }		}		return result; }; } Array.select = Array.filter; // Synonym

// Calls iterator on all elements of the array if (!Array.forEach) { Array.forEach = function (target, iterator, thisObject) { if (target === null) return; if (typeof target.forEach === 'function') { target.forEach(iterator, thisObject); return; }		if (typeof iterator !== 'function') throw new Error('Array.forEach: iterator must be a function'); var l = target.length; if (thisObject) iterator = iterator.bind(thisObject); for (var i = 0; i < l; i++) { if (i in target) iterator(target[i], i, target); }	}; }

// Returns true if predicate is true for every element of the array, false otherwise if (!Array.every) { Array.every = function (target, predicate, thisObject) { if (target === null) return true; if (typeof target.every === 'function') return target.every(predicate, thisObject); if (typeof predicate !== 'function') throw new Error('Array.every: predicate must be a function'); var l = target.length; if (thisObject) predicate = predicate.bind(thisObject); for (var i = 0; i < l; i++) { if (i in target && !predicate(target[i], i, target)) return false; }		return true; }; } Array.forAll = Array.every; // Synonym

// Returns true if predicate is true for at least one element of the array, false otherwise. if (!Array.some) { Array.some = function (target, predicate, thisObject) { if (target === null) return false; if (typeof target.some === 'function') return target.some(predicate, thisObject); if (typeof predicate !== 'function') throw new Error('Array.some: predicate must be a function'); var l = target.length; if (thisObject) predicate = predicate.bind(thisObject); for (var i = 0; i < l; i++) { if (i in target && predicate(target[i], i, target)) return true; }		return false; }; } Array.exists = Array.some; // Synonym

// Returns a new array built by applying mapper to all elements. if (!Array.map) { Array.map = function (target, mapper, thisObject) { if (target === null) return null; if (typeof target.map === 'function') return target.map(mapper, thisObject); if (typeof mapper !== 'function') throw new Error('Array.map: mapper must be a function'); var l = target.length, result = []; if (thisObject) mapper = mapper.bind(thisObject); for (var i = 0; i < l; i++) { if (i in target) result[i] = mapper(target[i], i, target); }		return result; }; }

if (!Array.indexOf) { Array.indexOf = function (target, elem, from) { if (target === null) return -1; if (typeof target.indexOf === 'function') return target.indexOf(elem, from); if (!target.length) return -1; var l = target.length; if (isNaN(from)) from = 0; else from = from || 0; from = (from < 0) ? Math.ceil(from) : Math.floor(from); if (from < 0) from += l;		if (from < 0) from = 0; while (from < l) { if (from in target && target[from] === elem) return from; from += 1; }		return -1; }; }

/** Additional Array enhancements ************/

Array.remove = function (target, elem) { var i = Array.indexOf(target, elem); if (i >= 0) target.splice(i, 1); };

Array.contains = function (target, elem) { return Array.indexOf(target, elem) >= 0; };

Array.flatten = function (target) { var result = []; Array.forEach(target, function (elem) {		result = result.concat(elem);	}); return result; };

// Calls selector on the array elements until it returns a non-null object // and then returns that object. If selector always returns null, any also // returns null. See also Array.map. Array.any = function (target, selector, thisObject) { if (target === null) return null; if (typeof selector !== 'function') throw new Error('Array.any: selector must be a function'); var l = target.length, result = null; if (thisObject) selector = selector.bind(thisObject); for (var i = 0; i < l; i++) { if (i in target) { result = selector(target[i], i, target); if (result !== null) return result; }	}	return null; };

// Return a contiguous array of the contents of source, which may be an array or pseudo-array, // basically anything that has a length and can be indexed. (E.g. live HTMLCollections, but also // Strings, or objects, or the arguments "variable". Array.make = function (source) {	if (!source || !source.length)		return null;	var result = [],		l = source.length;	for (var i = 0; i < l; i++) {		if (i in source)			result[result.length] = source[i];	}	return result; };

if (!window.LAPI) { var LAPI = window.LAPI = { Ajax: { getRequest: function { var request = null; try { request = new XMLHttpRequest; } catch (anything) { request = null; if (window.ActiveXObject) { if (!LAPI.Ajax.getRequest.msXMLHttpID) { var XHR_ids = ['MSXML2.XMLHTTP.6.0', 'MSXML2.XMLHTTP.3.0', 'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP']; for (var i = 0; i < XHR_ids.length && !request; i++) { try { request = new ActiveXObject(XHR_ids[i]); if (request) LAPI.Ajax.getRequest.msXMLHttpID = XHR_ids[i]; } catch (ex) { request = null; }							}							if (!request) LAPI.Ajax.getRequest.msXMLHttpID = null; } else if (LAPI.Ajax.getRequest.msXMLHttpID) { request = new ActiveXObject(LAPI.Ajax.getRequest.msXMLHttpID); }					} // end if IE				} // end try-catch return request; }		},

$: function (selector, doc, multi) { if (!selector || !selector.length) return null; doc = doc || document; if (typeof selector === 'string') { if (selector[0] === '#') selector = selector.substring(1); if (selector.length > 0) return doc.getElementById(selector); return null; } else { if (multi) { return Array.map(selector, function (id) {						return LAPI.$(id, doc);					}); }				return Array.any(selector, function (id) {					return LAPI.$(id, doc);				}); }		},

make: function (tag, attribs, css, doc) { doc = doc || document; if (!tag || !tag.length) throw new Error('No tag for LAPI.make'); var result = doc.createElement(tag); Object.mergeSet(attribs, result); Object.mergeSet(css, result.style); if (/^(form|input|button|select|textarea)$/.test(tag) &&				result.id && result.id.length > 0 && !result.name) result.name = result.id;

return result; },

formatException: function (ex, asDOM) { var name = ex.name || '', msg = ex.message || '', file = null, line = null; if (msg && msg.length > 0 && msg[0] === '#') { // User msg: don't confuse users with error locations. (Note: could also use				// custom exception types, but that doesn't work right on IE6.) msg = msg.substring(1); } else { file = ex.fileName || ex.sourceURL || null; // Gecko, Webkit, others line = ex.lineNumber || ex.line || null; // Gecko, Webkit, others }			if (name || msg) { if (!asDOM) { return (						'Exception ' + name + ': ' + msg + (file ? '\nFile ' + file + (line ? ' (' + line + ')' : ) : )); } else { var ex_msg = LAPI.make('div'); ex_msg.appendChild(document.createTextNode('Exception ' + name + ': ' + msg)); if (file) { ex_msg.appendChild(LAPI.make('br')); ex_msg.appendChild(document.createTextNode('File ' + file + (line ? ' (' + line + ')' : ''))); }					return ex_msg; }			} else { return null; }		}

}; } // end if (guard)

if (!LAPI.Browser) { // Yes, usually it's better to test for available features. But sometimes there's no // way around testing for specific browsers (differences in dimensions, layout errors, // etc.) LAPI.Browser = (function (agent) {			var result = {};			result.client = agent;			var m = agent.match(/applewebkit\/(\d+)/);			result.is_webkit = (m !== null);			result.is_safari = result.is_webkit && !agent.contains('spoofer');			result.webkit_version = (m ? parseInt(m[1]) : 0);			result.is_khtml =				navigator.vendor === 'KDE' || (document.childNodes && !document.all && !navigator.taintEnabled && navigator.accentColorName);			result.is_gecko =				agent.contains('gecko') && !/khtml|spoofer|netscape\/7\.0/.test(agent);			result.is_ff_1 = agent.contains('firefox/1');			result.is_ff_2 = agent.contains('firefox/2');			result.is_ff_ge_2 = /firefox\/[2-9]|minefield\/3/.test(agent);			result.is_ie = agent.contains('msie') || !!window.ActiveXObject;			result.is_ie_lt_7 = false;			if (result.is_ie) {				var version = /msie ((\d|\.)+)/.exec(agent);				result.is_ie_lt_7 = (version !== null && (parseFloat(version[1]) < 7));			}			result.is_opera = agent.contains('opera');			result.is_opera_ge_9 = false;			result.is_opera_95 = false;			if (result.is_opera) {				m = /opera\/((\d|\.)+)/.exec(agent);				result.is_opera_95 = m && (parseFloat(m[1]) >= 9.5);				result.is_opera_ge_9 = m && (parseFloat(m[1]) >= 9.0); }			result.is_mac = agent.contains('mac'); return result; }(navigator.userAgent.toLowerCase)); }

// end if (guard)

if (!LAPI.DOM) {

LAPI.DOM = { // IE6 doesn't have these Node constants in Node, so put them here ELEMENT_NODE: 1, ATTRIBUTE_NODE: 2, TEXT_NODE: 3, CDATA_SECTION_NODE: 4, ENTITY_REFERENCE_NODE: 5, ENTITY_NODE: 6, PROCESSING_INSTRUCTION_NODE: 7, COMMENT_NODE: 8, DOCUMENT_NODE: 9, DOCUMENT_TYPE_NODE: 10, DOCUMENT_FRAGMENT_NODE: 11, NOTATION_NODE: 12,

cleanAttributeName: function (attr_name) { if (!LAPI.Browser.is_ie) return attr_name; if (!LAPI.DOM.cleanAttributeName._names) { LAPI.DOM.cleanAttributeName._names = { 'class': 'className', cellspacing: 'cellSpacing', cellpadding: 'cellPadding', colspan: 'colSpan', maxlength: 'maxLength', readonly: 'readOnly', rowspan: 'rowSpan', tabindex: 'tabIndex', valign: 'vAlign' };			}			var cleaned = attr_name.toLowerCase; return LAPI.DOM.cleanAttributeName._names[cleaned] || cleaned; },

importNode: function (into, node, deep) { if (!node) return null; if (into.importNode) return into.importNode(node, deep); if (node.ownerDocument === into) return node.cloneNode(deep); var new_node = null; switch (node.nodeType) { case LAPI.DOM.ELEMENT_NODE: new_node = into.createElement(node.nodeName); Array.forEach(						node.attributes,						function (attr) {							if (attr && attr.nodeValue && attr.nodeValue.length > 0)								new_node.setAttribute(LAPI.DOM.cleanAttributeName(attr.name), attr.nodeValue);						}); new_node.style.cssText = node.style.cssText; if (deep) { Array.forEach(							node.childNodes,							function (child) {								var copy = LAPI.DOM.importNode(into, child, true);								if (copy)									new_node.appendChild(copy);							}); }					return new_node; case LAPI.DOM.TEXT_NODE: return into.createTextNode(node.nodeValue); case LAPI.DOM.CDATA_SECTION_NODE: return (into.createCDATASection ?						into.createCDATASection(node.nodeValue) :						into.createTextNode(node.nodeValue)); case LAPI.DOM.COMMENT_NODE: return into.createComment(node.nodeValue); default: return null; } // end switch },

parse: function (str, content_type) { function getDocument(str, content_type) { if (typeof DOMParser !== 'undefined') { var parser = new DOMParser; if (parser && parser.parseFromString) return parser.parseFromString(str, content_type); }				// We don't have DOMParser if (LAPI.Browser.is_ie) { var doc = null; // Apparently, these can be installed side-by-side. Try to get the newest one available. // Unfortunately, one finds a variety of version strings on the net. I have no idea which // ones are correct. if (!LAPI.DOM.parse.msDOMDocumentID) { // If we find a parser, we cache it. If we cannot find one, we also remember that. var parsers = ['MSXML6.DOMDocument', 'MSXML5.DOMDocument', 'MSXML4.DOMDocument', 'MSXML3.DOMDocument', 'MSXML2.DOMDocument.5.0', 'MSXML2.DOMDocument.4.0', 'MSXML2.DOMDocument.3.0', 'MSXML2.DOMDocument', 'MSXML.DomDocument', 'Microsoft.XmlDom']; for (var i = 0; i < parsers.length && !doc; i++) { try { doc = new ActiveXObject(parsers[i]); if (doc) LAPI.DOM.parse.msDOMDocumentID = parsers[i]; } catch (ex) { doc = null; }						}						if (!doc) LAPI.DOM.parse.msDOMDocumentID = null; } else if (LAPI.DOM.parse.msDOMDocumentID) { doc = new ActiveXObject(LAPI.DOM.parse.msDOMDocumentID); }					if (doc) { doc.async = false; doc.loadXML(str); return doc; }				}				// Try using a "data" URI (http://www.ietf.org/rfc/rfc2397). Reported to work on // older Safaris. content_type = content_type || 'application/xml'; var req = LAPI.Ajax.getRequest; if (req) { // Synchronous is OK, since "data" URIs are local req.open('GET', 'data:' + content_type + ';charset=utf-8,' + encodeURIComponent(str), false); if (req.overrideMimeType) req.overrideMimeType(content_type); req.send(null); return req.responseXML; }				return null; } // end getDocument

var doc = null;

try { doc = getDocument(str, content_type); } catch (ex) { doc = null; }			if (((!doc || !doc.documentElement) && (str.search(/^\s*(]*>\s*)?<!doctype\s+html/i) >= 0 || str.search(/^\s*= 0)) ||				(doc && (LAPI.Browser.is_ie && (!doc.documentElement && doc.parseError && doc.parseError.errorCode !== 0 && doc.parseError.reason.contains('Error processing resource') && doc.parseError.reason.contains('http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'))))) { // Either the text specified an (X)HTML document, but we failed to get a Document, or we				// hit the walls of the single-origin policy on IE which tries to get the DTD from the // URI specified... Let's fake a document: doc = LAPI.DOM.fakeHTMLDocument(str); }			return doc; },

parseHTML: function (str/*, sanity_check*/) { // Always use a faked document; parsing as XML and then treating the result as HTML doesn't work right with HTML5. return LAPI.DOM.fakeHTMLDocument(str); },

fakeHTMLDocument: function (str) { var body_tag = //.exec(str); if (!body_tag || !body_tag.length) return null; body_tag = body_tag.index + body_tag[0].length; // Index after the opening body tag var body_end = str.lastIndexOf(' '); if (body_end < 0) return null; var content = str.substring(body_tag, body_end); // Anything in between content = content.replace(//g, ''); // Sanitize: strip scripts return new LAPI.DOM.DocumentFacade(content); },

isValid: function (doc) { if (!doc) return doc; if (doc.parseError) { // IE				if (doc.parseError.errorCode !== 0) throw new Error('XML parse error: ' + doc.parseError.reason + ' line ' + doc.parseError.line + ' col ' + doc.parseError.linepos + '\nsrc = ' + doc.parseError.srcText);

} else { // FF... others? var root = doc.documentElement; if (/^parsererror$/i.test(root.tagName)) throw new Error('XML parse error: ' + root.getInnerText);

}			return doc; },

hasClass: function (node, className) { if (!node) return false; return (' ' + node.className + ' ').contains(' ' + className + ' '); },

setContent: function (node, content) { if (content === null) return node; LAPI.DOM.removeChildren(node); if (content.nodeName) { // presumably a DOM tree, like a span or a document fragment node.appendChild(content); } else if (node.innerHTML !== undefined) { node.innerHTML = content.toString; } else { node.appendChild(document.createTextNode(content.toString)); }			return node; },

makeImage: function (src, width, height, title, doc) { return LAPI.make(				'img', {					src: src,					width: String(width),					height: String(height),					title: title				}, doc); },

makeButton: function (id, text, f, submit, doc) { return LAPI.make(				'input', {					id: id || '',					type: (submit ? 'submit' : 'button'),					value: text,					onclick: f				}, doc); },

makeLabel: function (id, text, for_elem, doc) { var label = LAPI.make('label', {				id: id || '',				htmlFor: for_elem			}, null, doc); return LAPI.DOM.setContent(label, text); },

makeLink: function (url, text, tooltip, onclick, doc) { var lk = LAPI.make('a', {				href: url,				title: tooltip,				onclick: onclick			}, null, doc); return LAPI.DOM.setContent(lk, text || url); },

// Unfortunately, extending Node.prototype may not work on some browsers, // most notably (you've guessed it) IE...

getInnerText: function (node) { if (node.textContent) return node.textContent; if (node.innerText) return node.innerText; var result = ''; if (node.nodeType === LAPI.DOM.TEXT_NODE) { result = node.nodeValue; } else { Array.forEach(node.childNodes, function (elem) {					switch (elem.nodeType) {						case LAPI.DOM.ELEMENT_NODE:							result += LAPI.DOM.getInnerText(elem);							break;						case LAPI.DOM.TEXT_NODE:							result += elem.nodeValue;							break;					}				}); }			return result; },

removeNode: function (node) { if (node.parentNode) node.parentNode.removeChild(node); return node; },

removeChildren: function (node) { // if (typeof (node.innerHTML) !== 'undefined') node.innerHTML = ""; // Not a good idea. On IE this destroys all contained nodes, even if they're still referenced // from JavaScript! Can't have that... while (node.firstChild) node.removeChild(node.firstChild); return node; },

insertNode: function (node, before) { before.parentNode.insertBefore(node, before); return node; },

insertAfter: function (node, after) { var next = after.nextSibling; after.parentNode.insertBefore(node, next); return node; },

replaceNode: function (node, newNode) { node.parentNode.replaceChild(node, newNode); return newNode; },

isParentOf: function (parent, child) { while (child && child !== parent && child.parentNode) child = child.parentNode; return child === parent; },

// Property is to be in CSS style, e.g. 'background-color', not in JS style ('backgroundColor')! // Use standard 'cssFloat' for float property. currentStyle: function (elem, property) { function normalize(prop) { // Don't use a regexp with a lambda function (available only in JS 1.3)... and I once had a // case where IE6 goofed grossly with a lambda function. Since then I try to avoid those // (though they're neat). if (prop === 'cssFloat') return 'styleFloat'; // We'll try both variants below, standard first... var result = prop.split('-'); result = Array.map(result, function (s) {						if (s)							return s.capitalizeFirst;						else							return s;					}); result = result.join(''); return result.lowercaseFirst; }

if (elem.ownerDocument.defaultView && elem.ownerDocument.defaultView.getComputedStyle) { // Gecko etc.				if (property === 'cssFloat') property = 'float'; return elem.ownerDocument.defaultView.getComputedStyle(elem, null).getPropertyValue(property); } else { var result; if (elem.currentStyle) { // IE, has subtle differences to getComputedStyle result = elem.currentStyle[property] || elem.currentStyle[normalize(property)]; } else	{ // Not exactly right, but best effort result = elem.style[property] || elem.style[normalize(property)]; }				// Convert em etc. to pixels. Kudos to Dean Edwards; see // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 if (!/^\d+(px)?$/i.test(result) && /^\d/.test(result) && elem.runtimeStyle) { var style = elem.style.left, runtimeStyle = elem.runtimeStyle.left; elem.runtimeStyle.left = elem.currentStyle.left; elem.style.left = result || 0; result = elem.style.pixelLeft + 'px'; elem.style.left = style; elem.runtimeStyle.left = runtimeStyle; }			}		},

// Load a given image in a given size. Parameters: //  title //    Full title of the image, including the "File:" namespace //  url //    If !== null, URL of an existing thumb for that image. If width is null, may contain the url //    of the full image. //  width //    If !== null, desired width of the image, otherwise load the full image //  height //    If width !== null, height should also be set. //  auto_thumbs //    True if missing thumbnails are generated automatically. //  success //    Function to be called once the image is loaded. Takes one parameter: the IMG-tag of		//    the loaded image //  failure //    Function to be called if the image cannot be loaded. Takes one parameter: a string //    containing an error message. loadImage: function (title, url, width, height, auto_thumbs, success, failure) { if (auto_thumbs && url) { // MediaWiki-style with 404 handler. Set condition to false if your wiki does not have such a // setup. var img_src = null; if (width) { var i = url.lastIndexOf('/'); if (i >= 0) img_src = url.substring(0, i) + url.substring(i).replace(/^\/\d+px-/, '/' + width + 'px-');

} else if (url) { img_src = url; }				if (!img_src) { failure('Cannot load image from url ' + url); return; }				var img_loader = LAPI.make(						'img', {							src: img_src						}, {							position: 'absolute',							top: '0px',							left: '0px',							display: 'none'						}); if (width) img_loader.width = String(width); if (height) img_loader.height = String(height); LAPI.Evt.attach(img_loader, 'load', function {					success(img_loader);				}); document.body.appendChild(img_loader); // Now the browser goes loading the image } else { // No url to work with. Use parseWikitext to have a thumb generated an to get its URL. LAPI.Ajax.parseWikitext(					 + width + 'px' : ) + '',					function (html/*, failureFunc*/) {						var dummy =							LAPI.make( 'div', null, { position: 'absolute', top: '0px', left: '0px', display: 'none' });						document.body.appendChild(dummy); // Now start loading the image						dummy.innerHTML = html;						var imgs = dummy.getElementsByTagName('img');						LAPI.Evt.attach( imgs[0], 'load', function { success(imgs[0]); LAPI.DOM.removeNode(dummy); });					},					function (request/*, json_result*/) {						failure('Image loading failed: ' + request.status + ' ' + request.statusText);					}, false // Not as preview					, null // user language: don't care					, null // on page: don't care					, 3600 // Cache for an hour				); }		}

}; // end LAPI.DOM

LAPI.DOM.DocumentFacade = function { this.initialize.apply(this, arguments); };

LAPI.DOM.DocumentFacade.prototype = { initialize: function (text) { // It's not a real document, but it will behave like one for our purposes. this.documentElement = LAPI.make('div', null, {				display: 'none',				position: 'absolute'			}); this.body = LAPI.make('div', null, {				position: 'relative'			}); this.documentElement.appendChild(this.body); document.body.appendChild(this.documentElement); this.body.innerHTML = text; // Find all forms var forms = document.getElementsByTagName('form'), self = this; this.forms = Array.select(forms, function (f) {				return LAPI.DOM.isParentOf(self.body, f);			}); // Konqueror 4.2.3/4.2.4 clears form.elements when the containing div is removed from the // parent document?! if (!LAPI.Browser.is_khtml) { LAPI.DOM.removeNode(this.documentElement); } else { this.dispose = function { LAPI.DOM.removeNode(this.documentElement); };				// Since we must leave the stuff *in* the original document on Konqueror, we'll also need a // dispose routine... what an ugly hack. }			this.allIDs = {}; this.isFake = true; },

createElement: function (tag) { return document.createElement(tag); },		createDocumentFragment: function { return document.createDocumentFragment; },		createTextNode: function (text) { return document.createTextNode(text); },		createComment: function (text) { return document.createComment(text); },		createCDATASection: function (text) { return document.createCDATASection(text); },		createAttribute: function (name) { return document.createAttribute(name); },		createEntityReference: function (name) { return document.createEntityReference(name); },		createProcessingInstruction: function (target, data) { return document.createProcessingInstruction(target, data); },

getElementsByTagName: function (tag) { // Grossly inefficient, but deprecated anyway var res = [];

function traverse(node, tag) { if (node.nodeName.toLowerCase === tag) res[res.length] = node; var curr = node.firstChild; while (curr) { traverse(curr, tag); curr = curr.nextSibling; }			}			traverse(this.body, tag.toLowerCase); return res; },

getElementById: function (id) { function traverse(elem, id) { if (elem.id === id) return elem; var res = null, curr = elem.firstChild; while (curr && !res) { res = traverse(curr, id); curr = curr.nextSibling; }				return res; }

if (!this.allIDs[id]) this.allIDs[id] = traverse(this.body, id); return this.allIDs[id]; }

// ...NS operations omitted

}; // end DocumentFacade

if (document.importNode) { LAPI.DOM.DocumentFacade.prototype.importNode = function (node, deep) { document.importNode(node, deep); };	}

} // end if (guard)

if (!LAPI.WP) {

LAPI.WP = {

getContentDiv: function (doc) { // Monobook, modern, classic skins return LAPI.$(['bodyContent', 'mw_contentholder', 'article'], doc); },

fullImageSizeFromPage: function (doc) { // Get the full img size. This is screenscraping :-( but there are times where you don't			// want to get this info from the server using an Ajax call.			// Note: we get the size from the file history table because the text just below the image			// is all scrambled on RTL wikis. For instance, on ar-WP, it is			// "\u200f (1,806 × 1,341 بكسل، حجم الملف: 996 كيلوبايت، نوع الملف: image/jpeg) and with uselang=en,			// it is at ar-WP "\u200f (1,806 × 1,341 pixels, file size: 996 KB, MIME type: image/jpeg)"			// However, in the file history table, it looks good no matter the language and writing			// direction.			// Update: this fails on e.g. ar-WP because someone had the great idea to use localized			// numerals, but the digit transform table is empty!			var result = {					width: 0,					height: 0				},				file_hist = LAPI.$('mw-imagepage-section-filehistory', doc);			if (!file_hist)				return result;			try {				var $file_curr = $ ? $(file_hist).find('td.filehistory-selected') : document.getElementsByClassName(file_hist, 'td', 'filehistory-selected'); // Did they change the column order here? It once was nextSibling.nextSibling... but somehow // the thumbnails seem to be gone... Right: // http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/ImagePage.php?r1=52385&r2=53130 file_hist = LAPI.DOM.getInnerText($file_curr[0].nextSibling); if (!file_hist.contains('×')) { file_hist = LAPI.DOM.getInnerText($file_curr[0].nextSibling.nextSibling); if (!file_hist.contains('×')) file_hist = null; }			} catch (ex) { return result; }			// Now we have "number×number" followed by something arbitrary if (file_hist) { file_hist = file_hist.split('×', 2); result.width = parseInt(file_hist.shift.replace(/[^0-9]/g, ''), 10); // Height is a bit more difficult because e.g. uselang=eo uses a space as the thousands // separator. Hence we have to extract this more carefully file_hist = file_hist.pop; // Everything after the "×" // Remove any white space embedded between digits file_hist = file_hist.replace(/(\d)\s*(\d)/g, '$1$2'); file_hist = file_hist.split(' ', 2).shift.replace(/[^0-9]/g, ''); result.height = parseInt(file_hist, 10); if (isNaN(result.width) || isNaN(result.height)) { result = { width: 0, height: 0 };				}			}			return result; },

getPreviewImage: function (title, doc) { var file_div = LAPI.$('file', doc); if (!file_div) return null; // Catch page without file... var imgs = file_div.getElementsByTagName('img'); title = title || mw.config.get('wgTitle'); for (var i = 0; i < imgs.length; i++) { var src = imgs[i].getAttribute('src', 2); if (src && src.search(/^data/)) { src = decodeURIComponent(src).replace('%26', '&'); if (!src.search(new RegExp('^' + LAPI_file_store + '.*/' + title.replace(/ /g, '_').escapeRE + '(/.*)?$'))) return imgs[i]; }			}			return null; },

pageFromLink: function (lk) { if (!lk) return null; var href = lk.getAttribute('href', 2); if (!href) return null; // This is a bit tricky to get right, because 'wgScript' can be a substring prefix of // article path, or vice versa. var script = mw.config.get('wgScript') + '?'; if (href.startsWith(script) || href.startsWith(mw.config.get('wgServer') + script) || mw.config.get('wgServer').startsWith('//') && href.startsWith(document.location.protocol + mw.config.get('wgServer') + script)) { // href="/w/index.php?title=..." return href.getParamValue('title'); }			// Now try article path: href="/wiki/..." var prefix = mw.config.get('wgArticlePath').replace('$1', ''); if (!href.startsWith(prefix)) prefix = mw.config.get('wgServer') + prefix; // Fully expanded URL? if (!href.startsWith(prefix) && prefix.startsWith('//')) prefix = document.location.protocol + prefix; // Protocol-relative 'wgServer'? if (href.startsWith(prefix)) return decodeURIComponent(href.substring(prefix.length)); // Do we have variants? var variants = mw.config.get('wgVariantArticlePath'); if (variants && variants.length > 0) { var re = new RegExp(variants.escapeRE.replace('\\$2', '[^\\/]*').replace('\\$1', '(.*)')), m = re.exec(href); if (m && m.length > 1) return decodeURIComponent(m[m.length - 1]); }			// Finally alternative action paths var actions = mw.config.get('wgActionPaths'); if (actions) { for (var i = 0; i < actions.length; i++) { var p = actions[i]; if (p && p.length > 0) { p = p.replace('$1', ''); if (!href.startsWith(p)) p = mw.config.get('wgServer') + p;						if (!href.startsWith(p) && p.startsWith('//')) p = document.location.protocol + p;						if (href.startsWith(p)) return decodeURIComponent(href.substring(p.length)); }				}			}			return null; },

revisionFromHtml: function (htmlOfPage) { var revision_id = null; if (window.RLCONF) // MW 1.32+ revision_id = htmlOfPage.match(/RLCONF=\{.*"wgCurRevisionId"\s*:\s*(\d+),/); else if (window.mediaWiki) // MW 1.17+ revision_id = htmlOfPage.match(/(?:mediaWiki|mw).config.set\(\{.*"wgCurRevisionId"\s*:\s*(\d+),/);			else // MW < 1.17				revision_id = htmlOfPage.match(/wgCurRevisionId\s*=\s*(\d+)[;,]/);			if (revision_id)				revision_id = parseInt(revision_id[1], 10);			return revision_id;		}

}; // end LAPI.WP

} // end if (guard)

if (!LAPI.Ajax.doAction) {

mw.loader.load( '/index.php?title=MediaWiki:AjaxSubmit.js&action=raw&ctype=text/javascript' ); // Legacy code: ajaxSubmit

LAPI.Ajax.getXML = function (request, failureFunc) { var doc = null; if (request.responseXML && request.responseXML.documentElement) { doc = request.responseXML; } else { try { doc = LAPI.DOM.parse(request.responseText, 'text/xml'); } catch (ex) { if (typeof failureFunc === 'function') failureFunc(request, ex); doc = null; }		}		if (doc) { try { doc = LAPI.DOM.isValid(doc); } catch (ex) { if (typeof failureFunc === 'function') failureFunc(request, ex); doc = null; }		}		return doc; };

LAPI.Ajax.getHTML = function (request, failureFunc, sanity_check) { // Konqueror sometimes has severe problems with responseXML. It does set it, but getElementById // may fail to find elements known to exist. var doc = null; // Always use our own parser instead of responseXML; that doesn't work right with HTML5. (It did work with XHTML, though.) // if (   request.responseXML && request.responseXML.documentElement		//      && request.responseXML.documentElement.tagName === 'HTML'		//      && (!sanity_check || request.responseXML.getElementById (sanity_check) !== null)		//     ) // {		//    doc = request.responseXML; // } else { try { doc = LAPI.DOM.parseHTML(request.responseText, sanity_check); if (!doc) throw new Error('#Could not understand request result'); } catch (ex) { if (typeof failureFunc === 'function') failureFunc(request, ex); doc = null; }		// }		if (doc) { try { doc = LAPI.DOM.isValid(doc); } catch (ex) { if (typeof failureFunc === 'function') failureFunc(request, ex); doc = null; }		}		if (doc === null) return doc; // We've gotten XML. There is a subtle difference between XML and (X)HTML concerning leading newlines in textareas: // XML is required to pass through any whitespace (http://www.w3.org/TR/2004/REC-xml-20040204/#sec-white-space), whereas // HTML may or must not (e.g. http://www.w3.org/TR/html4/appendix/notes.html#h-B.3.1, though it is unclear whether that		// really applies to the content of a textarea, but the draft HTML 5 spec explicitly says that the first newline in a		// is swallowed in HTML:		// http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#element-restrictions). //  Because of the latter MW1.18+ adds a newline after the start tag if the value starts with a newline. That // solves bug 12130 (leading newlines swallowed), but since XML passes us this extra newline, we might end up adding a // leading newline upon each edit. //  Let's try to make sure that all textarea's values are as they should be in HTML. // Note: since the above change to always use our own parser, which always returns a faked HTML document, this should be // unnecessary since doc.isFake should always be true. if (!LAPI.Ajax.getHTML.extraNewlineRE) { // Feature detection. Compare value after parsing with value after .innerHTML. LAPI.Ajax.getHTML.extraNewlineRE = null; // Don't know; hence do nothing try { var testTA = ' \nTest ', testString = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n' + '\n' + ' Test    ' + testTA + ' \n ', testDoc = LAPI.DOM.parseHTML(testString, 'test'), testVal = String(testDoc.getElementById('test').value); if (testDoc.dispose) testDoc.dispose; var testDiv = LAPI.make('div', null, {					display: 'none'				}); document.body.appendChild(testDiv); testDiv.innerHTML = testTA; if (testDiv.firstChild.value !== testVal) { LAPI.Ajax.getHTML.extraNewlineRE = /^\r?\n/; if (testDiv.firstChild.value !== testVal.replace(LAPI.Ajax.getHTML.extraNewlineRE, '')) { // Huh? Not the expected difference: go back to "don't know" mode LAPI.Ajax.getHTML.extraNewlineRE = null; }				}				LAPI.DOM.removeNode(testDiv); } catch (any) { LAPI.Ajax.getHTML.extraNewlineRE = null; }		}		if (!doc.isFake && LAPI.Ajax.getHTML.extraNewlineRE !== null) { // If have a "fake" doc, then we did parse through .innerHTML anyway. No need to fix anything. // (Hm. Maybe we should just always use a fake doc?) var tas = doc.getElementsByTagName('textarea'); for (var i = 0, l = tas.length; i < l; i++) tas[i].value = tas[i].value.replace(LAPI.Ajax.getHTML.extraNewlineRE, '');

}		return doc; };

LAPI.Ajax.get = function (uri, params, success, failure, config) { var original_failure = failure; if (!failure || typeof failure !== 'function') failure = function {}; if (!success || typeof success !== 'function') throw new Error('No success function supplied for LAPI.Ajax.get ' + uri + ' with arguments ' + params.toString); var request = LAPI.Ajax.getRequest; if (!request) { failure(request); return; }		var args = '', question_mark = uri.indexOf('?'); if (question_mark) { args = uri.substring(question_mark + 1); uri = uri.substring(0, question_mark); }		if (params !== null) { if (typeof params === 'string' && params.length > 0) { args += (args.length > 0 ? '&' : '') + ((params[0] === '&' || params[0] === '?') ? params.substring(1) : params); // Must already be encoded! } else { for (var param in params) { args += (args.length > 0 ? '&' : '') + param; if (params[param] !== null) args += '=' + encodeURIComponent(params[param]); }			}		}		var method; if (uri.startsWith('//')) uri = document.location.protocol + uri; // Avoid protocol-relative URIs (IE7 bug) if (uri.length + args.length + 1 < (LAPI.Browser.is_ie ? 2040 : 4080)) {			// Both browsers and web servers may have limits on URL length. IE has a limit of 2083 characters // (2048 in the path part), and the WMF servers seem to impose a limit of 4kB. method = 'GET'; uri += '?' + args; args = null; } else { method = 'POST'; // We'll lose caching, but at least we can make the request. }		request.open(method, uri, true); request.setRequestHeader('Pragma', 'cache=yes'); request.setRequestHeader(			'Cache-Control', 'no-transform' + (params && params.maxage ? ', max-age=' + params.maxage : '') + (params && params.smaxage ? ', s-maxage=' + params.smaxage : '')); if (config) { for (var conf in config) { if (conf === 'overrideMimeType') { if (config[conf] && config[conf].length > 0 && request.overrideMimeType) request.overrideMimeType(config[conf]); } else { request.setRequestHeader(conf, config[conf]); }			}		}		if (args) request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); request.onreadystatechange = function { if (request.readyState !== 4) return; // Wait until the request has completed. try { if (request.status !== 200) throw new Error('#Request to server failed. Status: ' + request.status + ' ' + request.statusText + ' URI: ' + uri); if (!request.responseText) throw new Error('#Empty response from server for request ' + uri); } catch (ex) { failure(request, ex); return; }			success(request, original_failure); };		request.send(args); };

LAPI.Ajax.getPage = function (page, action, params, success, failure) { var uri = mw.config.get('wgServer') + mw.config.get('wgScript') + '?title=' + encodeURIComponent(page) + (action ? '&action=' + action : ''); LAPI.Ajax.get(uri, params, success, failure, {			overrideMimeType: 'application/xml'		}); };

// modify is supposed to save the changes at the end, e.g. using LAPI.Ajax.submit. // modify is called with three parameters: the document, possibly the form, and the optional // failure function. The failure function is called with the request as the first parameter, // and possibly an exception as the second parameter. LAPI.Ajax.doAction = function (page, action, form, modify, failure) { if (!page || !action || !modify || typeof modify !== 'function') throw new Error('Parameter inconsistency in LAPI.Ajax.doAction.'); var original_failure = failure; if (!failure || typeof failure !== 'function') failure = function {}; LAPI.Ajax.getPage(			page, action, null, // No additional parameters			function (request, failureFunc) {				var doc = null,					the_form = null,					revision_id = null;				try {					// Convert responseText into DOM tree.					doc = LAPI.Ajax.getHTML(request, failureFunc, form);					if (!doc)						return;					var err_msg = LAPI.$('permissions-errors', doc);					if (err_msg)						throw new Error('#' + LAPI.DOM.getInnerText(err_msg));					if (form) {						the_form = LAPI.$(form, doc);						if (!the_form)							throw new Error('#Server reply does not contain mandatory form.');						the_form.wpWatchthis.checked = !!document.getElementById('ca-unwatch');					}					revision_id = LAPI.WP.revisionFromHtml(request.responseText);				} catch (ex) {					failureFunc(request, ex);					return;				}				modify(doc, the_form, original_failure, revision_id);			}, failure); }; // end LAPI.Ajax.doAction

LAPI.Ajax.submit = function (form, after_submit) { try { ajaxSubmit(form, null, after_submit, true); // Legacy code from MediaWiki:AjaxSubmit } catch (ex) { after_submit(null, ex); }	}; // end LAPI.Ajax.submit

LAPI.Ajax.editPage = function (page, modify, failure) { LAPI.Ajax.doAction(page, 'edit', 'editform', modify, failure); }; // end LAPI.Ajax.editPage

LAPI.Ajax.checkEdit = function (request) { if (!request) return true; // Check for previews (session token lost?) or edit forms (edit conflict). try { var doc = LAPI.Ajax.getHTML(request, function {				throw new Error('Cannot check HTML');			}); if (!doc) return false; return LAPI.$(['wikiPreview', 'editform'], doc) === null; } catch (anything) { return false; }	}; // end LAPI.Ajax.checkEdit

LAPI.Ajax.submitEdit = function (form, success, failure) { if (!success || typeof success !== 'function') success = function {}; if (!failure || typeof failure !== 'function') failure = function {}; LAPI.Ajax.submit(			form,			function (request, ex) {				if (ex) {					failure(request, ex);				} else {					var successful = false;					try {						successful = request && request.status === 200 && LAPI.Ajax.checkEdit(request);					} catch (some_error) {						failure(request, some_error);						return;					}					if (successful)						success(request);					else						failure(request);				}			}); }; // end LAPI.Ajax.submitEdit

LAPI.Ajax.apiGet = function (action, params, success, failure) { var original_failure = failure; if (!failure || typeof failure !== 'function') failure = function {}; if (!success || typeof success !== 'function') throw new Error('No success function supplied for LAPI.Ajax.apiGet ' + action + ' with arguments ' + params.toString); var is_json = false; if (params !== null) { if (typeof params === 'string') { if (!/format=[^&]+/.test(params)) params += '&format=json'; is_json = /format=json(&|$)/.test(params); // Exclude jsonfm, which actually serves XHTML } else { if (typeof params.format !== 'string' || !params.format.length) params.format = 'json'; is_json = params.format === 'json'; }		}		var uri = mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php' + (action ? '?action=' + action : ''); LAPI.Ajax.get(			uri, params,			function (request, failureFunc) {				if (is_json && request.responseText.trimLeft[0] !== '{') {					failureFunc(request);				} else {                   var json;                    try {                        json = (is_json ? eval('(' + request.responseText.trimLeft + ')') : null)                   } catch (e) {                        json = null;                    }					success( request, json, original_failure);				}			}, failure); }; // end LAPI.Ajax.apiGet

LAPI.Ajax.parseWikitext = function (wikitext, success, failure, as_preview, user_language, on_page, cache) { if (!failure || typeof failure !== 'function') failure = function {}; if (!success || typeof success !== 'function') throw new Error('No success function supplied for parseWikitext'); if (!wikitext && !on_page) throw new Error('No wikitext or page supplied for parseWikitext'); var params = null; if (!wikitext) { params = { pst: null, page: on_page };		} else { params = { pst: null, // Do the pre-save-transform: Pipe magic, tilde expansion, etc. text: (as_preview ? ' \{\{MediaWiki:Previewnote/' +				(user_language || mw.config.get('wgUserLanguage')) + '\}\}  \n' : '') + wikitext + (as_preview ? '  ' : ''), title: on_page || mw.config.get('wgPageName') || 'API' };		}		params.prop = 'text'; params.uselang = user_language || mw.config.get('wgUserLanguage'); // see bugzilla 22764 if (cache && /^\d+$/.test(cache = cache.toString)) { params.maxage = cache; params.smaxage = cache; }		LAPI.Ajax.apiGet(			'parse', params,			function (req, json_result, failureFunc) {				// Success.				if (!json_result || !json_result.parse || !json_result.parse.text) {					failureFunc(req, json_result);					return;				}				success(json_result.parse.text['*'], failureFunc);			}, failure); }; // end LAPI.Ajax.parseWikitext

// Throbber backward-compatibility LAPI.Ajax.injectSpinner = function (/* elementBefore, id*/) {}; // No-op, replaced as appropriate below. LAPI.Ajax.removeSpinner = function (/* id*/) {}; // No-op, replaced as appropriate below.

if (!window.jQuery || !window.mediaWiki || !mw.loader) { // Assume old-stlye if (window.injectSpinner) LAPI.Ajax.injectSpinner = window.injectSpinner; if (window.removeSpinner) LAPI.Ajax.removeSpinner = window.removeSpinner; } else { mw.loader.using('jquery.spinner', function {			LAPI.Ajax.injectSpinner = function (elementBefore, id) {				$(elementBefore).injectSpinner(id);			};			LAPI.Ajax.removeSpinner = function (id) {				$.removeSpinner(id);			};		}); }

} // end if (guard)

if (!LAPI.Pos) {

LAPI.Pos = { // Returns the global coordinates of the mouse pointer within the document. mousePosition: function (evt) { if (!evt || (!evt.pageX && !evt.clientX)) // No way to calculate a mouse pointer position return null; if (evt.pageX.evt) { return { x: evt.pageX, y: evt.pageY };			}

var offset = LAPI.Pos.scrollOffset, mouse_delta = LAPI.Pos.mouse_offset, coor_x = evt.clientX + offset.x - mouse_delta.x,				coor_y = evt.clientY + offset.y - mouse_delta.y;			return { x: coor_x, y: coor_y };		},

// Operations on document level:

// Returns the scroll offset of the whole document (in other words, the coordinates		// of the top left corner of the viewport). scrollOffset: function { return { x: LAPI.Pos.getScroll('Left'), y: LAPI.Pos.getScroll('Top') };		},

getScroll: function (what) { var s = 'scroll' + what; return (document.documentElement ? document.documentElement[s] : 0) || document.body[s] || 0; },

// Returns the size of the viewport (result.x is the width, result.y the height). viewport: function { return { x: LAPI.Pos.getViewport('Width'), y: LAPI.Pos.getViewport('Height') };		},

getViewport: function (what) { if (LAPI.Browser.is_opera_95 && what === 'Height' || LAPI.Browser.is_safari && !document.evaluate) return window['inner' + what]; var s = 'client' + what; if (LAPI.Browser.is_opera) return document.body[s]; return (document.documentElement ? document.documentElement[s] : 0) || document.body[s] || 0; },

// Operations on DOM nodes

position: (function {			// The following is the jQuery.offset implementation. We cannot use jQuery yet in globally			// activated scripts (it has strange side effects for Opera 8 users who can't log in anymore, // and it breaks the search box for some users). Note that jQuery does not support Opera 8.			// Until the WMF servers serve jQuery by default, this copy from the jQuery 1.3.2 sources is			// needed here. If and when we have jQuery available officially, the whole thing here can be			// replaced by "var tmp = jQuery (node).offset; return {x:tmp.left, y:tmp.top};"			// Kudos to the jQuery development team. Any errors in this adaptation are my own. (Lupo, // 2009-08-24).

var data = null;

function jQuery_init { data = {}; // Capability check from jQuery. var body = document.body, container = document.createElement('div'), html = '  ', rules = { position: 'absolute', visibility: 'hidden', top: 0, left: 0, margin: 0, border: 0, width: '1px', height: '1px' };				Object.merge(rules, container.style);

container.innerHTML = html; body.insertBefore(container, body.firstChild); var innerDiv = container.firstChild, checkDiv = innerDiv.firstChild, td = innerDiv.nextSibling.firstChild.firstChild;

data.doesNotAddBorder = (checkDiv.offsetTop !== 5); data.doesAddBorderForTableAndCells = (td.offsetTop === 5);

innerDiv.style.overflow = 'hidden'; innerDiv.style.position = 'relative'; data.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5);

var bodyMarginTop = body.style.marginTop; body.style.marginTop = '1px'; data.doesNotIncludeMarginInBodyOffset = (body.offsetTop === 0); body.style.marginTop = bodyMarginTop;

body.removeChild(container); }

function jQuery_offset(node) { if (node === node.ownerDocument.body) return jQuery_bodyOffset(node); if (node.getBoundingClientRect) { var box = node.getBoundingClientRect, scroll = LAPI.Pos.scrollOffset; return { x: (box.left + scroll.x), y: (box.top + scroll.y)					}; }				if (!data) jQuery_init; var elem = node, offsetParent = elem.offsetParent, // prevOffsetParent = elem, doc = elem.ownerDocument, prevComputedStyle = doc.defaultView.getComputedStyle(elem, null), computedStyle, top = elem.offsetTop, left = elem.offsetLeft; while ((elem = elem.parentNode) && elem !== doc.body && elem !== doc.documentElement) { computedStyle = doc.defaultView.getComputedStyle(elem, null); top -= elem.scrollTop; left -= elem.scrollLeft; if (elem === offsetParent) { top += elem.offsetTop; left += elem.offsetLeft; if (data.doesNotAddBorder && !(data.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.tagName))) { top += parseInt(computedStyle.borderTopWidth, 10) || 0; left += parseInt(computedStyle.borderLeftWidth, 10) || 0; }						// prevOffsetParent = offsetParent; offsetParent = elem.offsetParent; }					if (data.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== 'visible') { top += parseInt(computedStyle.borderTopWidth, 10) || 0; left += parseInt(computedStyle.borderLeftWidth, 10) || 0; }					prevComputedStyle = computedStyle; }

if (prevComputedStyle.position === 'relative' || prevComputedStyle.position === 'static') { top += doc.body.offsetTop; left += doc.body.offsetLeft; }				if (prevComputedStyle.position === 'fixed') { top += Math.max(doc.documentElement.scrollTop, doc.body.scrollTop); left += Math.max(doc.documentElement.scrollLeft, doc.body.scrollLeft); }				return { x: left, y: top };			}

function jQuery_bodyOffset(body) { if (!data) jQuery_init; var top = body.offsetTop, left = body.offsetLeft; if (data.doesNotIncludeMarginInBodyOffset) { top += parseInt(LAPI.DOM.currentStyle(body, 'margin-top'), 10) || 0; left += parseInt(LAPI.DOM.currentStyle(body, 'margin-left'), 10) || 0; }				return { x: left, y: top };			}

return jQuery_offset; }),

isWithin: function (node, x, y) { if (!node || !node.parentNode) return false; var pos = LAPI.Pos.position(node); return (x === null || x > pos.x && x < pos.x + node.offsetWidth) && (y === null || y > pos.y && y < pos.y + node.offsetHeight); },

// Private:

// IE has some strange offset... mouse_offset: function { if (LAPI.Browser.is_ie) { var doc_elem = document.documentElement; if (doc_elem) { if (typeof doc_elem.getBoundingClientRect === 'function') { var tmp = doc_elem.getBoundingClientRect; return { x: tmp.left, y: tmp.top };					} else { return { x: doc_elem.clientLeft, y: doc_elem.clientTop };					}				}			}			return { x: 0, y: 0 };		}

}; // end LAPI.Pos

} // end if (guard)

if (!LAPI.Evt) {

LAPI.Evt = { listenTo: function (object, node, evt, f, capture) { var listener = LAPI.Evt.makeListener(object, f); LAPI.Evt.attach(node, evt, listener, capture); },

attach: function (node, evt, f, capture) { if (node.attachEvent) node.attachEvent('on' + evt, f); else if (node.addEventListener) node.addEventListener(evt, f, capture); else node['on' + evt] = f;		},

remove: function (node, evt, f, capture) { if (node.detachEvent) node.detachEvent('on' + evt, f); else if (node.removeEventListener) node.removeEventListener(evt, f, capture); else node['on' + evt] = null; },

makeListener: function (obj, listener) { // Some hacking around to make sure 'this' is set correctly var object = obj, f = listener; return function (evt) { return f.apply(object, [evt || window.event]); };			// Alternative implementation: // var f = listener.bind (obj); // return function (evt) { return f (evt || window.event); }; },

kill: function (evt) { if (typeof evt.preventDefault === 'function') { evt.stopPropagation; evt.preventDefault; // Don't follow the link } else if (evt.cancelBubble.evt) { // IE... evt.cancelBubble = true; }			return false; // Don't follow the link (IE) }

}; // end LAPI.Evt

} // end if (guard)

if (!LAPI.Edit) { LAPI.Edit = function { this.initialize.apply(this, arguments); };

LAPI.Edit.SAVE = 1; LAPI.Edit.PREVIEW = 2; LAPI.Edit.REVERT = 4; LAPI.Edit.CANCEL = 8;

LAPI.Edit.prototype = { initialize: function (initial_text, columns, rows, labels, handlers) { var my_labels = { box: null, preview: null, save: 'Save', cancel: 'Cancel', nullsave: null, revert: null, post: null };			if (labels) my_labels = Object.merge(labels, my_labels); this.labels = my_labels; this.timestamp = (new Date).getTime; this.id = 'simpleedit_' + this.timestamp; this.view = LAPI.make('div', {				id: this.id			}, {				marginRight: '1em'			}); // Somehow, the textbox extends beyond the bounding box of the view. Don't know why, but // adding a small margin fixes the layout more or less. this.form = LAPI.make(					'form', {						id: this.id + '_form',						action: '',						onsubmit: (function {})					}); if (my_labels.box) { var label = LAPI.make('div'); label.appendChild(LAPI.DOM.makeLabel(this.id + '_label', my_labels.box, this.id + '_text')); this.form.appendChild(label); }			this.textarea = LAPI.make(					'textarea', {						id: this.id + '_text',						cols: columns,						rows: rows,						value: (initial_text ? initial_text.toString : '')					}); LAPI.Evt.attach(this.textarea, 'keyup', LAPI.Evt.makeListener(this, this.text_changed)); // Catch cut/copy/paste through the context menu. Some browsers support oncut, oncopy, // onpaste events for this, but since that's only IE, FF 3, Safari 3, and Chrome, we // cannot rely on this. Instead, we check again as soon as we leave the textarea. Only // minor catch is that on FF 3, the next focus target is determined before the blur event // fires. Since in practice save will always be enabled, this shouldn't be a problem. LAPI.Evt.attach(this.textarea, 'mouseout', LAPI.Evt.makeListener(this, this.text_changed)); LAPI.Evt.attach(this.textarea, 'blur', LAPI.Evt.makeListener(this, this.text_changed)); this.form.appendChild(this.textarea); this.form.appendChild(LAPI.make('br')); this.preview_section = LAPI.make('div', null, {					borderBottom: '1px solid #88a',					display: 'none'				}); this.view.insertBefore(this.preview_section, this.view.firstChild); this.save = LAPI.DOM.makeButton(this.id + '_save', my_labels.save, LAPI.Evt.makeListener(this, this.do_save)); this.form.appendChild(this.save); if (my_labels.preview) { this.preview = LAPI.DOM.makeButton(this.id + '_preview', my_labels.preview, LAPI.Evt.makeListener(this, this.do_preview)); this.form.appendChild(this.preview); }			this.cancel = LAPI.DOM.makeButton(this.id + '_cancel', my_labels.cancel, LAPI.Evt.makeListener(this, this.do_cancel)); this.form.appendChild(this.cancel); this.view.appendChild(this.form); if (my_labels.post) { this.post_text = LAPI.DOM.setContent(LAPI.make('div'), my_labels.post); this.view.appendChild(this.post_text); }			if (handlers) Object.merge(handlers, this); if (typeof this.ongettext !== 'function') { this.ongettext = function (text) { return text; };			} // Default: no modifications this.current_mask = LAPI.Edit.SAVE + LAPI.Edit.PREVIEW + LAPI.Edit.REVERT + LAPI.Edit.CANCEL; if ((!initial_text || !initial_text.trim.length) && this.preview) this.preview.disabled = true; if (my_labels.revert) { this.revert = LAPI.DOM.makeButton(this.id + '_revert', my_labels.revert, LAPI.Evt.makeListener(this, this.do_revert)); this.form.insertBefore(this.revert, this.cancel); }			this.original_text = ''; },

getView: function { return this.view; },

getText: function { return this.ongettext(this.textarea.value); },

setText: function (text) { this.textarea.value = text; this.original_text = text; this.text_changed; },

changeText: function (text) { this.textarea.value = text; this.text_changed; },

hidePreview: function { this.preview_section.style.display = 'none'; if (this.onpreview) this.onpreview(this); },

showPreview: function { this.preview_section.style.display = ''; if (this.onpreview) this.onpreview(this); },

setPreview: function (html) { if (html.nodeName) { LAPI.DOM.removeChildren(this.preview_section); this.preview_section.appendChild(html); } else { this.preview_section.innerHTML = html; }		},

busy: function (show) { if (show) LAPI.Ajax.injectSpinner(this.cancel, this.id + '_spinner'); else LAPI.Ajax.removeSpinner(this.id + '_spinner'); },

do_save: function (/* evt*/) { if (this.onsave) this.onsave(this); return true; },

do_revert: function (/* evt*/) { this.changeText(this.original_text); return true; },

do_cancel: function (/* evt*/) { if (this.oncancel) this.oncancel(this); return true; },

do_preview: function (/* evt*/) { var self = this; this.busy(true); LAPI.Ajax.parseWikitext(				this.getText,				function (text/*, failureFunc*/) {					self.busy(false);					self.setPreview(text);					self.showPreview;				},				function (/* req, json_result*/) {					// Error. TODO: user feedback?					self.busy(false);				}, true, mw.config.get('wgUserLanguage') || null, mw.config.get('wgPageName') || null); return true; },

enable: function (bit_set) { var call_text_changed = false; this.current_mask = bit_set; this.save.disabled = ((bit_set & LAPI.Edit.SAVE) === 0); this.cancel.disabled = ((bit_set & LAPI.Edit.CANCEL) === 0); if (this.preview) { if ((bit_set & LAPI.Edit.PREVIEW) === 0) this.preview.disabled = true; else call_text_changed = true; }			if (this.revert) { if ((bit_set & LAPI.Edit.REVERT) === 0) this.revert.disabled = true; else call_text_changed = true; }			if (call_text_changed) this.text_changed; },

text_changed: function (/* evt*/) { var text = this.textarea.value; text = text.trim; var length = text.length; if (this.preview && (this.current_mask & LAPI.Edit.PREVIEW) !== 0) { // Preview is basically enabled this.preview.disabled = (length <= 0); }			if (this.labels.nullsave) this.save.value = (length > 0) ? this.labels.save : this.labels.nullsave; if (this.revert) { this.revert.disabled = (text === this.original_text || this.textarea.value === this.original_text); }			return true; }

}; // end LAPI.Edit

} // end if (guard) //