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.

if (typeof (LAPI_file_store) == 'undefined')

var LAPI_file_store = "(https?:)?//upload\\.wikimedia\\.org/";

// 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.

if (!Object.clone) {

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;

};

}

if (!Object.merge) {

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;

};

}

if (!Object.mergeSome) {

Object.mergeSome = function (from, into, includeInherited, predicate)

{

if (!from) return into;

if (typeof (predicate) == 'undefined')

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;

};

}

if (!Object.mergeSet) {

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 both ends of the string.

// If no characters are given, defaults to removing whitespace.

if (!String.prototype.trim) {

String.prototype.trim = function (chars) {

if (!chars) return this.replace (/^\s+|\s+$/g, "");

return this.trimRight (chars).trimLeft (chars);

};

}

// 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 () + ']+'), "");

};

}

if (!String.prototype.trimFront)

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 () + ']+$'), "");

};

}

if (!String.prototype.trimEnd)

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) {

var last = this.lastIndexOf (suffix);

return last !== -1 && last + suffix.length == this.length;

};

}

// Returns true if the string contains s.

if (!String.prototype.contains) {

String.prototype.contains = function (s) {

return this.indexOf (s) >= 0;

};

}

// Replace all occurrences of a string pattern by replacement.

if (!String.prototype.replaceAll) {

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.

if (!String.prototype.stringifyJS) {

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.

if (!String.prototype.escapeRE) {

String.prototype.escapeRE = function () {

return this.replace (/([\\{}()|.?*+^$\[\]])/g, "\\$1");

};

}

if (!String.prototype.escapeXML) {

String.prototype.escapeXML = function (quot, apos) {

var s = this.replace (/&/g, '&')

.replace (/\xa0/g, ' ')

.replace (/

.replace (/>/g, '>');

if (quot) s = s.replace (/\"/g, '"'); // " // Fix syntax coloring

if (apos) s = s.replace (/\'/g, '''); // ' // Fix syntax coloring

return s;

};

}

if (!String.prototype.decodeXML) {

String.prototype.decodeXML = function () {

return this.replace(/"/g, '"')

.replace(/'/g, "'")

.replace(/>/g, '>')

.replace(/</g, '<')

.replace(/ /g, '\xa0')

.replace(/&/g, '&');

};

}

if (!String.prototype.capitalizeFirst) {

String.prototype.capitalizeFirst = function () {

return this.substring (0, 1).toUpperCase() + this.substring (1);

};

}

if (!String.prototype.lowercaseFirst) {

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.

if (!String.prototype.getParamValue) {

String.prototype.getParamValue = function (param) {

var re = new RegExp ('[&?]' + param.escapeRE () + '=([^&#]*)');

var m = re.exec (this);

if (m && m.length >= 2) return decodeURIComponent (m[1]);

return null;

};

}

if (!String.getParamValue) {

String.getParamValue = function (param, url)

{

if (typeof (url) == 'undefined' || url === null) url = document.location.href;

try {

return url.getParamValue (param);

} catch (e) {

return null;

}

};

}

/** Function enhancements ************/

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;

var result = [];

if (thisObject) predicate = predicate.bind (thisObject);

for (var i=0; l && i < l; i++) {

if (i in target) {

var curr = target[i];

if (predicate (curr, i, target)) result[result.length] = curr;

}

}

return result;

};

}

if (!Array.select)

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; l && 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; l && i < l; i++) {

if (i in target && !predicate (target[i], i, target)) return false;

}

return true;

};

}

if (!Array.forAll)

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; l && i < l; i++) {

if (i in target && predicate (target[i], i, target)) return true;

}

return false;

};

}

if (!Array.exists)

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;

var result = [];

if (thisObject) mapper = mapper.bind (thisObject);

for (var i=0; l && 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 (typeof (target.length) == 'undefined') 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;

};

}

if (!Array.lastIndexOf) {

Array.lastIndexOf = function (target, elem, from)

{

if (target === null) return -1;

if (typeof (target.lastIndexOf) == 'function') return target.lastIndexOf (elem, from);

if (typeof (target.length) == 'undefined') return -1;

var l = target.length;

if (isNaN (from)) from = l-1; else from = from || (l-1);

from = (from < 0) ? Math.ceil (from) : Math.floor (from);

if (from < 0) from += l; else if (from >= l) from = l-1;

while (from >= 0) {

if (from in target && target[from] === elem) return from;

from -= 1;

}

return -1;

};

}

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

if (!Array.remove) {

Array.remove = function (target, elem) {

var i = Array.indexOf (target, elem);

if (i >= 0) target.splice (i, 1);

};

}

if (!Array.contains) {

Array.contains = function (target, elem) {

return Array.indexOf (target, elem) >= 0;

};

}

if (!Array.flatten) {

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.

if (!Array.any) {

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;

var result = null;

if (thisObject) selector = selector.bind (thisObject);

for (var i=0; l && 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".

if (!Array.make) {

Array.make = function (source)

{

if (!source || typeof (source.length) == 'undefined') return null;

var result = [];

var l = source.length;

for (var i=0; i < l; i++) {

if (i in source) result[result.length] = source[i];

}

return result;

};

}

if (typeof (window.LAPI) == 'undefined') {

window.LAPI = {

Ajax :

{

getRequest : function ()

{

var request = null;

try {

request = new XMLHttpRequest();

} catch (anything) {

request = null;

if (!!window.ActiveXObject) {

if (typeof (LAPI.Ajax.getRequest.msXMLHttpID) == 'undefined') {

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 == 0) return null;

doc = doc || document;

if (typeof (selector) == 'string') {

if (selector.charAt (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 == 0) 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 || "";

var msg = ex.message || "";

var file = null;

var line = null;

if (msg && msg.length > 0 && msg.charAt (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 (typeof (LAPI.Browser) == 'undefined') {

// 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 (typeof (LAPI.DOM) == 'undefined') {

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 (typeof (LAPI.DOM.parse.msDOMDocumentID) == 'undefined') {

// 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*)?= 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 == 0) 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 (typeof (doc.parseError) != 'undefined') { // 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 (typeof (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: "" + width, height : "" + 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 (element, 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 (element.ownerDocument.defaultView

&& element.ownerDocument.defaultView.getComputedStyle)

{ // Gecko etc.

if (property == 'cssFloat') property = 'float';

return element.ownerDocument.defaultView.getComputedStyle (element, null).getPropertyValue (property);

} else {

var result;

if (element.currentStyle) { // IE, has subtle differences to getComputedStyle

result = element.currentStyle[property] || element.currentStyle[normalize (property)];

} else // Not exactly right, but best effort

result = element.style[property] || element.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) && element.runtimeStyle) {

var style = element.style.left;

var runtimeStyle = element.runtimeStyle.left;

element.runtimeStyle.left = element.currentStyle.left;

element.style.left = result || 0;

result = elem.style.pixelLeft + "px";

element.style.left = style;

element.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 = "" + width;

if (height) img_loader.height = "" + 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');

var 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;

var 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 (typeof (LAPI.WP) == 'undefined') {

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

// "‏ (1,806 × 1,341 بكسل، حجم الملف: 996 كيلوبايت، نوع الملف: image/jpeg) and with uselang=en,

// it is at ar-WP "‏ (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};

var file_hist = LAPI.$ ('mw-imagepage-section-filehistory', doc);

if (!file_hist) return result;

try {

var $file_curr = window.jQuery ? $(file_hist).find('td.filehistory-selected') : 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 = decodeURIComponent (imgs[i].getAttribute ('src', 2)).replace ('%26', '&');

if (src.search (new RegExp ('^' + LAPI_file_store + '.*/' + title.replace (/ /g, '_').escapeRE () + '(/.*)?$')) == 0)

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

// wgArticlePath, 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 wgArticlePath: 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', "(.*)"));

var 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.mediaWiki) { // MW 1.17+

revision_id = htmlOfPage.match (/(mediaWiki|mw).config.set\(\{.*"wgCurRevisionId"\s*:\s*(\d+),/);

if (revision_id) revision_id = parseInt (revision_id[2], 10);

} 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 (typeof (LAPI.Ajax.doAction) == 'undefined') {

importScript ('MediaWiki:AjaxSubmit.js'); // 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

// ';

var testString = '\n'

+ '\n'

+ 'Test

' + testTA + '
\n'

+ '';

var testDoc = LAPI.DOM.parseHTML (testString, 'test');

var testVal = "" + 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 = "";

var 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.charAt (0) == '&' || params.charAt (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;

var the_form = null;

var 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.');

}

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 == 0) 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().charAt (0) != '{') {

failureFunc (request);

} else {

success (

request

, (is_json ? eval ('(' + request.responseText.trimLeft() + ')') : null)

, 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')) +'\}\}'

+ '\<\/div>\\n'

: "")

+ wikitext

+ (as_preview ? '\<\/div\>\

\<\/div\>\<\/div\>' : "")

,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 (typeof window.jQuery == 'undefined' || typeof window.mediaWiki == 'undefined' || typeof window.mediaWiki.loader == 'undefined') {

// Assume old-stlye

if (typeof window.injectSpinner != 'undefined') {

LAPI.Ajax.injectSpinner = window.injectSpinner;

}

if (typeof window.removeSpinner != 'undefined') {

LAPI.Ajax.removeSpinner = window.removeSpinner;

}

} else {

window.mediaWiki.loader.using('jquery.spinner', function () {

LAPI.Ajax.injectSpinner = function (elementBefore, id) {

window.jQuery(elementBefore).injectSpinner(id);

}

LAPI.Ajax.removeSpinner = function (id) {

window.jQuery.removeSpinner(id);

}

});

}

} // end if (guard)

if (typeof (LAPI.Pos) == 'undefined') {

LAPI.Pos =

{

// Returns the global coordinates of the mouse pointer within the document.

mousePosition : function (evt)

{

if (!evt || (typeof (evt.pageX) == 'undefined' && typeof (evt.clientX) == 'undefined'))

// No way to calculate a mouse pointer position

return null;

if (typeof (evt.pageX) != 'undefined')

return { x : evt.pageX, y : evt.pageY };

var offset = LAPI.Pos.scrollOffset ();

var mouse_delta = LAPI.Pos.mouse_offset ();

var coor_x = evt.clientX + offset.x - mouse_delta.x;

var 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;

var container = document.createElement('div');

var html =

'

+ 'cellpadding="0" cellspacing="0">

';

var 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;

var checkDiv = innerDiv.firstChild;

var 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 ();

var scroll = LAPI.Pos.scrollOffset ();

return {x : (box.left + scroll.x), y : (box.top + scroll.y)};

}

if (!data) jQuery_init ();

var elem = node;

var offsetParent = elem.offsetParent;

var prevOffsetParent = elem;

var doc = elem.ownerDocument;

var prevComputedStyle = doc.defaultView.getComputedStyle(elem, null);

var computedStyle;

var top = elem.offsetTop;

var 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 (typeof (LAPI.Evt) == 'undefined') {

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 (typeof (evt.cancelBubble) != 'undefined') { // IE...

evt.cancelBubble = true;

}

return false; // Don't follow the link (IE)

}

}; // end LAPI.Evt

} // end if (guard)

if (typeof (LAPI.Edit) == 'undefined') {

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 #8888aa', 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 == 0) && 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) {

if (length > 0) {

this.save.value = this.labels.save;

} else {

this.save.value = 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)