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,    '&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; }; }

if (!String.prototype.decodeXML) { String.prototype.decodeXML = function { return this.replace(/&quot;/g, '"')              .replace(/&apos;/g, "'")               .replace(/&gt;/g,   '>')               .replace(/&lt;/g,   '<')               .replace(/ /g, '\xa0')               .replace(/&amp;/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*)?<!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 == 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  // 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 (typeof (LAPI.Ajax.getHTML.extraNewlineRE) == 'undefined') { // 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 '; var 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' + ' ';     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 = '   '; 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)