/*
Small JS library containing stuff I use often.
Author: [[User:Lupo]], June 2009
License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
Choose whichever license of these you like best :-)
Includes the following components:
- Object enhancements (clone, merge)
- String enhancements (trim, ...)
- Array enhancements (JS 1.6)
- Function enhancements (bind)
- LAPI Most basic DOM functions: $ (getElementById), make
- LAPI.Ajax Ajax request implementation, tailored for MediaWiki/WMF sites
- LAPI.Browser Browser detection (general)
- LAPI.DOM DOM helpers, including a cross-browser DOM parser
- LAPI.WP MediaWiki/WMF-specific DOM routines
- LAPI.Edit Simple editor implementation with save, cancel, preview (for WMF sites)
- LAPI.Evt Event handler routines (general)
- LAPI.Pos Position calculations (general)
*/
//
// Global: importScript (from wiki.js, for MediaWiki:AjaxSubmit.js)
// Configuration: set this to the URL of your image server. The value is a string representation
// of a regular expression. For instance, for Wikia, use "http://images\\d\\.wikia\\.nocookie\\.net".
// Remember to double-escape the backslash.
/* global importScript, LAPI, ajaxSubmit */
/* jshint unused:false, laxcomma:true, smarttabs:true, loopfunc:true, forin:false */
/* eslint-disable one-var, vars-on-top, camelcase, no-use-before-define, eqeqeq, no-bitwise */
if (window.LAPI_file_store === undefined)
var LAPI_file_store = '(https?:)?//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.
Object.clone = function (source, includeInherited) {
if (!source)
return null;
var result = {};
for (var key in source) {
if (includeInherited || source.hasOwnProperty(key))
result[key] = source[key];
}
return result;
};
Object.merge = function (from, into, includeInherited) {
if (!from)
return into;
for (var key in from) {
if (includeInherited || from.hasOwnProperty(key))
into[key] = from[key];
}
return into;
};
Object.mergeSome = function (from, into, includeInherited, predicate) {
if (!from)
return into;
if (!predicate)
return Object.merge(from, into, includeInherited);
for (var key in from) {
if ((includeInherited || from.hasOwnProperty(key)) && predicate(from, into, key))
into[key] = from[key];
}
return into;
};
Object.mergeSet = function (from, into, includeInherited) {
return Object.mergeSome(from, into, includeInherited, function (src, tgt, key) {
return src[key] !== null;
});
};
/** String enhancements (JavaScript 1.6) ************/
// Removes given characters from the beginning of the string.
// If no characters are given, defaults to removing whitespace.
if (!String.prototype.trimLeft) {
String.prototype.trimLeft = function (chars) {
if (!chars)
return this.replace(/^\s\s*/, '');
return this.replace(new RegExp('^[' + chars.escapeRE() + ']+'), '');
};
}
String.prototype.trimFront = String.prototype.trimLeft; // Synonym
// Removes given characters from the end of the string.
// If no characters are given, defaults to removing whitespace.
if (!String.prototype.trimRight) {
String.prototype.trimRight = function (chars) {
if (!chars)
return this.replace(/\s\s*$/, '');
return this.replace(new RegExp('[' + chars.escapeRE() + ']+$'), '');
};
}
String.prototype.trimEnd = String.prototype.trimRight; // Synonym
/** Further String enhancements ************/
// Returns true if the string begins with prefix.
if (!String.prototype.startsWith) {
String.prototype.startsWith = function (prefix) {
return this.indexOf(prefix) === 0;
};
}
// Returns true if the string ends in suffix
if (!String.prototype.endsWith) {
String.prototype.endsWith = function (suffix) {
return this.lastIndexOf(suffix) + suffix.length === this.length;
};
}
// Returns true if the string contains s.
String.prototype.contains = function (s) {
return this.indexOf(s) >= 0;
};
// Replace all occurrences of a string pattern by replacement.
String.prototype.replaceAll = function (pattern, replacement) {
return this.split(pattern).join(replacement);
};
// Escape all backslashes and single or double quotes such that the result can
// be used in JavaScript inside quotes or double quotes.
String.prototype.stringifyJS = function () {
return this.replace(/([\\'"]|%5C|%27|%22)/g, '\\$1') // ' // Fix syntax coloring
.replace(/\n/g, '\\n');
};
// Escape all RegExp special characters such that the result can be safely used
// in a RegExp as a literal.
String.prototype.escapeRE = function () {
return this.replace(/([\\{}()|.?*+^$[\]])/g, '\\$1');
};
String.prototype.escapeXML = function (quot, apos) {
var s = this.replace(/&/g, '&')
.replace(/\xa0/g, ' ')
.replace(//g, '>');
if (quot)
s = s.replace(/"/g, '"'); // " // Fix syntax coloring
if (apos)
s = s.replace(/'/g, '''); // ' // Fix syntax coloring
return s;
};
String.prototype.decodeXML = function () {
return this.replace(/"/g, '"')
.replace(/'/g, '\'')
.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/ /g, '\xa0')
.replace(/&/g, '&');
};
String.prototype.capitalizeFirst = function () {
return this.substring(0, 1).toUpperCase() + this.substring(1);
};
String.prototype.lowercaseFirst = function () {
return this.substring(0, 1).toLowerCase() + this.substring(1);
};
// This is actually a function on URLs, but since URLs typically are strings in
// JavaScript, let's include this one here, too.
String.prototype.getParamValue = function (param) {
var re = new RegExp('[&?]' + param.escapeRE() + '=([^&#]*)'),
m = re.exec(this);
if (m && m.length >= 2)
return decodeURIComponent(m[1]);
return null;
};
String.getParamValue = function (param, url) {
url = url || document.location.href;
try {
return url.getParamValue(param);
} catch (e) {
return null;
}
};
/** Function enhancements (JavaScript 1.8.5.) ************/
if (!Function.prototype.bind) {
// Return a function that calls the function with 'this' bound to 'thisObject'
Function.prototype.bind = function (thisObject) {
var f = this,
obj = thisObject,
slice = Array.prototype.slice,
prefixedArgs = slice.call(arguments, 1);
return function () {
return f.apply(obj, prefixedArgs.concat(slice.call(arguments)));
};
};
}
/** Array enhancements (JavaScript 1.6) ************/
// Note that contrary to JS 1.6, we treat the thisObject as optional.
// Don't add to the prototype, that would break for (var key in array) loops!
// Returns a new array containing only those elements for which predicate
// is true.
if (!Array.filter) {
Array.filter = function (target, predicate, thisObject) {
if (target === null)
return null;
if (typeof target.filter === 'function')
return target.filter(predicate, thisObject);
if (typeof predicate !== 'function')
throw new Error('Array.filter: predicate must be a function');
var l = target.length,
result = [];
if (thisObject)
predicate = predicate.bind(thisObject);
for (var i = 0; i < l; i++) {
if (i in target) {
var curr = target[i];
if (predicate(curr, i, target))
result[result.length] = curr;
}
}
return result;
};
}
Array.select = Array.filter; // Synonym
// Calls iterator on all elements of the array
if (!Array.forEach) {
Array.forEach = function (target, iterator, thisObject) {
if (target === null)
return;
if (typeof target.forEach === 'function') {
target.forEach(iterator, thisObject);
return;
}
if (typeof iterator !== 'function')
throw new Error('Array.forEach: iterator must be a function');
var l = target.length;
if (thisObject)
iterator = iterator.bind(thisObject);
for (var i = 0; i < l; i++) {
if (i in target)
iterator(target[i], i, target);
}
};
}
// Returns true if predicate is true for every element of the array, false otherwise
if (!Array.every) {
Array.every = function (target, predicate, thisObject) {
if (target === null)
return true;
if (typeof target.every === 'function')
return target.every(predicate, thisObject);
if (typeof predicate !== 'function')
throw new Error('Array.every: predicate must be a function');
var l = target.length;
if (thisObject)
predicate = predicate.bind(thisObject);
for (var i = 0; i < l; i++) {
if (i in target && !predicate(target[i], i, target))
return false;
}
return true;
};
}
Array.forAll = Array.every; // Synonym
// Returns true if predicate is true for at least one element of the array, false otherwise.
if (!Array.some) {
Array.some = function (target, predicate, thisObject) {
if (target === null)
return false;
if (typeof target.some === 'function')
return target.some(predicate, thisObject);
if (typeof predicate !== 'function')
throw new Error('Array.some: predicate must be a function');
var l = target.length;
if (thisObject)
predicate = predicate.bind(thisObject);
for (var i = 0; i < l; i++) {
if (i in target && predicate(target[i], i, target))
return true;
}
return false;
};
}
Array.exists = Array.some; // Synonym
// Returns a new array built by applying mapper to all elements.
if (!Array.map) {
Array.map = function (target, mapper, thisObject) {
if (target === null)
return null;
if (typeof target.map === 'function')
return target.map(mapper, thisObject);
if (typeof mapper !== 'function')
throw new Error('Array.map: mapper must be a function');
var l = target.length,
result = [];
if (thisObject)
mapper = mapper.bind(thisObject);
for (var i = 0; i < l; i++) {
if (i in target)
result[i] = mapper(target[i], i, target);
}
return result;
};
}
if (!Array.indexOf) {
Array.indexOf = function (target, elem, from) {
if (target === null)
return -1;
if (typeof target.indexOf === 'function')
return target.indexOf(elem, from);
if (!target.length)
return -1;
var l = target.length;
if (isNaN(from))
from = 0;
else
from = from || 0;
from = (from < 0) ? Math.ceil(from) : Math.floor(from);
if (from < 0)
from += l;
if (from < 0)
from = 0;
while (from < l) {
if (from in target && target[from] === elem)
return from;
from += 1;
}
return -1;
};
}
/** Additional Array enhancements ************/
Array.remove = function (target, elem) {
var i = Array.indexOf(target, elem);
if (i >= 0)
target.splice(i, 1);
};
Array.contains = function (target, elem) {
return Array.indexOf(target, elem) >= 0;
};
Array.flatten = function (target) {
var result = [];
Array.forEach(target, function (elem) {
result = result.concat(elem);
});
return result;
};
// Calls selector on the array elements until it returns a non-null object
// and then returns that object. If selector always returns null, any also
// returns null. See also Array.map.
Array.any = function (target, selector, thisObject) {
if (target === null)
return null;
if (typeof selector !== 'function')
throw new Error('Array.any: selector must be a function');
var l = target.length,
result = null;
if (thisObject)
selector = selector.bind(thisObject);
for (var i = 0; i < l; i++) {
if (i in target) {
result = selector(target[i], i, target);
if (result !== null)
return result;
}
}
return null;
};
// Return a contiguous array of the contents of source, which may be an array or pseudo-array,
// basically anything that has a length and can be indexed. (E.g. live HTMLCollections, but also
// Strings, or objects, or the arguments "variable".
Array.make = function (source) {
if (!source || !source.length)
return null;
var result = [],
l = source.length;
for (var i = 0; i < l; i++) {
if (i in source)
result[result.length] = source[i];
}
return result;
};
if (!window.LAPI) {
var LAPI = window.LAPI = {
Ajax: {
getRequest: function () {
var request = null;
try {
request = new XMLHttpRequest();
} catch (anything) {
request = null;
if (window.ActiveXObject) {
if (!LAPI.Ajax.getRequest.msXMLHttpID) {
var XHR_ids = ['MSXML2.XMLHTTP.6.0', 'MSXML2.XMLHTTP.3.0', 'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP'];
for (var i = 0; i < XHR_ids.length && !request; i++) {
try {
request = new ActiveXObject(XHR_ids[i]);
if (request)
LAPI.Ajax.getRequest.msXMLHttpID = XHR_ids[i];
} catch (ex) {
request = null;
}
}
if (!request)
LAPI.Ajax.getRequest.msXMLHttpID = null;
} else if (LAPI.Ajax.getRequest.msXMLHttpID) {
request = new ActiveXObject(LAPI.Ajax.getRequest.msXMLHttpID);
}
} // end if IE
} // end try-catch
return request;
}
},
$: function (selector, doc, multi) {
if (!selector || !selector.length)
return null;
doc = doc || document;
if (typeof selector === 'string') {
if (selector[0] === '#')
selector = selector.substring(1);
if (selector.length > 0)
return doc.getElementById(selector);
return null;
} else {
if (multi) {
return Array.map(selector, function (id) {
return LAPI.$(id, doc);
});
}
return Array.any(selector, function (id) {
return LAPI.$(id, doc);
});
}
},
make: function (tag, attribs, css, doc) {
doc = doc || document;
if (!tag || !tag.length)
throw new Error('No tag for LAPI.make');
var result = doc.createElement(tag);
Object.mergeSet(attribs, result);
Object.mergeSet(css, result.style);
if (/^(form|input|button|select|textarea)$/.test(tag) &&
result.id && result.id.length > 0 && !result.name)
result.name = result.id;
return result;
},
formatException: function (ex, asDOM) {
var name = ex.name || '',
msg = ex.message || '',
file = null,
line = null;
if (msg && msg.length > 0 && msg[0] === '#') {
// User msg: don't confuse users with error locations. (Note: could also use
// custom exception types, but that doesn't work right on IE6.)
msg = msg.substring(1);
} else {
file = ex.fileName || ex.sourceURL || null; // Gecko, Webkit, others
line = ex.lineNumber || ex.line || null; // Gecko, Webkit, others
}
if (name || msg) {
if (!asDOM) {
return (
'Exception ' + name + ': ' + msg + (file ? '\nFile ' + file + (line ? ' (' + line + ')' : '') : ''));
} else {
var ex_msg = LAPI.make('div');
ex_msg.appendChild(document.createTextNode('Exception ' + name + ': ' + msg));
if (file) {
ex_msg.appendChild(LAPI.make('br'));
ex_msg.appendChild(document.createTextNode('File ' + file + (line ? ' (' + line + ')' : '')));
}
return ex_msg;
}
} else {
return null;
}
}
};
}
// end if (guard)
if (!LAPI.Browser) {
// Yes, usually it's better to test for available features. But sometimes there's no
// way around testing for specific browsers (differences in dimensions, layout errors,
// etc.)
LAPI.Browser =
(function (agent) {
var result = {};
result.client = agent;
var m = agent.match(/applewebkit\/(\d+)/);
result.is_webkit = (m !== null);
result.is_safari = result.is_webkit && !agent.contains('spoofer');
result.webkit_version = (m ? parseInt(m[1]) : 0);
result.is_khtml =
navigator.vendor === 'KDE' || (document.childNodes && !document.all && !navigator.taintEnabled && navigator.accentColorName);
result.is_gecko =
agent.contains('gecko') && !/khtml|spoofer|netscape\/7\.0/.test(agent);
result.is_ff_1 = agent.contains('firefox/1');
result.is_ff_2 = agent.contains('firefox/2');
result.is_ff_ge_2 = /firefox\/[2-9]|minefield\/3/.test(agent);
result.is_ie = agent.contains('msie') || !!window.ActiveXObject;
result.is_ie_lt_7 = false;
if (result.is_ie) {
var version = /msie ((\d|\.)+)/.exec(agent);
result.is_ie_lt_7 = (version !== null && (parseFloat(version[1]) < 7));
}
result.is_opera = agent.contains('opera');
result.is_opera_ge_9 = false;
result.is_opera_95 = false;
if (result.is_opera) {
m = /opera\/((\d|\.)+)/.exec(agent);
result.is_opera_95 = m && (parseFloat(m[1]) >= 9.5);
result.is_opera_ge_9 = m && (parseFloat(m[1]) >= 9.0);
}
result.is_mac = agent.contains('mac');
return result;
}(navigator.userAgent.toLowerCase()));
}
// end if (guard)
if (!LAPI.DOM) {
LAPI.DOM = {
// IE6 doesn't have these Node constants in Node, so put them here
ELEMENT_NODE: 1,
ATTRIBUTE_NODE: 2,
TEXT_NODE: 3,
CDATA_SECTION_NODE: 4,
ENTITY_REFERENCE_NODE: 5,
ENTITY_NODE: 6,
PROCESSING_INSTRUCTION_NODE: 7,
COMMENT_NODE: 8,
DOCUMENT_NODE: 9,
DOCUMENT_TYPE_NODE: 10,
DOCUMENT_FRAGMENT_NODE: 11,
NOTATION_NODE: 12,
cleanAttributeName: function (attr_name) {
if (!LAPI.Browser.is_ie)
return attr_name;
if (!LAPI.DOM.cleanAttributeName._names) {
LAPI.DOM.cleanAttributeName._names = {
'class': 'className',
cellspacing: 'cellSpacing',
cellpadding: 'cellPadding',
colspan: 'colSpan',
maxlength: 'maxLength',
readonly: 'readOnly',
rowspan: 'rowSpan',
tabindex: 'tabIndex',
valign: 'vAlign'
};
}
var cleaned = attr_name.toLowerCase();
return LAPI.DOM.cleanAttributeName._names[cleaned] || cleaned;
},
importNode: function (into, node, deep) {
if (!node)
return null;
if (into.importNode)
return into.importNode(node, deep);
if (node.ownerDocument === into)
return node.cloneNode(deep);
var new_node = null;
switch (node.nodeType) {
case LAPI.DOM.ELEMENT_NODE:
new_node = into.createElement(node.nodeName);
Array.forEach(
node.attributes,
function (attr) {
if (attr && attr.nodeValue && attr.nodeValue.length > 0)
new_node.setAttribute(LAPI.DOM.cleanAttributeName(attr.name), attr.nodeValue);
});
new_node.style.cssText = node.style.cssText;
if (deep) {
Array.forEach(
node.childNodes,
function (child) {
var copy = LAPI.DOM.importNode(into, child, true);
if (copy)
new_node.appendChild(copy);
});
}
return new_node;
case LAPI.DOM.TEXT_NODE:
return into.createTextNode(node.nodeValue);
case LAPI.DOM.CDATA_SECTION_NODE:
return (into.createCDATASection ?
into.createCDATASection(node.nodeValue) :
into.createTextNode(node.nodeValue));
case LAPI.DOM.COMMENT_NODE:
return into.createComment(node.nodeValue);
default:
return null;
} // end switch
},
parse: function (str, content_type) {
function getDocument(str, content_type) {
if (typeof DOMParser !== 'undefined') {
var parser = new DOMParser();
if (parser && parser.parseFromString)
return parser.parseFromString(str, content_type);
}
// We don't have DOMParser
if (LAPI.Browser.is_ie) {
var doc = null;
// Apparently, these can be installed side-by-side. Try to get the newest one available.
// Unfortunately, one finds a variety of version strings on the net. I have no idea which
// ones are correct.
if (!LAPI.DOM.parse.msDOMDocumentID) {
// If we find a parser, we cache it. If we cannot find one, we also remember that.
var parsers = ['MSXML6.DOMDocument', 'MSXML5.DOMDocument', 'MSXML4.DOMDocument', 'MSXML3.DOMDocument', 'MSXML2.DOMDocument.5.0', 'MSXML2.DOMDocument.4.0', 'MSXML2.DOMDocument.3.0', 'MSXML2.DOMDocument', 'MSXML.DomDocument', 'Microsoft.XmlDom'];
for (var i = 0; i < parsers.length && !doc; i++) {
try {
doc = new ActiveXObject(parsers[i]);
if (doc)
LAPI.DOM.parse.msDOMDocumentID = parsers[i];
} catch (ex) {
doc = null;
}
}
if (!doc)
LAPI.DOM.parse.msDOMDocumentID = null;
} else if (LAPI.DOM.parse.msDOMDocumentID) {
doc = new ActiveXObject(LAPI.DOM.parse.msDOMDocumentID);
}
if (doc) {
doc.async = false;
doc.loadXML(str);
return doc;
}
}
// Try using a "data" URI (http://www.ietf.org/rfc/rfc2397). Reported to work on
// older Safaris.
content_type = content_type || 'application/xml';
var req = LAPI.Ajax.getRequest();
if (req) {
// Synchronous is OK, since "data" URIs are local
req.open('GET', 'data:' + content_type + ';charset=utf-8,' + encodeURIComponent(str), false);
if (req.overrideMimeType)
req.overrideMimeType(content_type);
req.send(null);
return req.responseXML;
}
return null;
} // end getDocument
var doc = null;
try {
doc = getDocument(str, content_type);
} catch (ex) {
doc = null;
}
if (((!doc || !doc.documentElement) && (str.search(/^\s*(]*>\s*)?= 0 || str.search(/^\s*= 0)) ||
(doc && (LAPI.Browser.is_ie && (!doc.documentElement && doc.parseError && doc.parseError.errorCode !== 0 && doc.parseError.reason.contains('Error processing resource') && doc.parseError.reason.contains('http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'))))) {
// Either the text specified an (X)HTML document, but we failed to get a Document, or we
// hit the walls of the single-origin policy on IE which tries to get the DTD from the
// URI specified... Let's fake a document:
doc = LAPI.DOM.fakeHTMLDocument(str);
}
return doc;
},
parseHTML: function (str/* , sanity_check*/) {
// Always use a faked document; parsing as XML and then treating the result as HTML doesn't work right with HTML5.
return LAPI.DOM.fakeHTMLDocument(str);
},
fakeHTMLDocument: function (str) {
var body_tag = //.exec(str);
if (!body_tag || !body_tag.length)
return null;
body_tag = body_tag.index + body_tag[0].length; // Index after the opening body tag
var body_end = str.lastIndexOf('');
if (body_end < 0)
return null;
var content = str.substring(body_tag, body_end); // Anything in between
content = content.replace(/