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*(
|| 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 = /
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(/