MediaWiki:LAPI.js
Jump to navigation
Jump to search
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
/*
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)
*/
// <nowiki>
// 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, '<')
.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*(<xml[^>]*>\s*)?<!doctype\s+html/i) >= 0 || str.search(/^\s*<html/i) >= 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 = /<body.*?>/.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('</body>');
if (body_end < 0)
return null;
var content = str.substring(body_tag, body_end); // Anything in between
content = content.replace(/<script(.|\s)*?\/script>/g, ''); // Sanitize: strip scripts
return new LAPI.DOM.DocumentFacade(content);
},
isValid: function (doc) {
if (!doc)
return doc;
if (doc.parseError) { // IE
if (doc.parseError.errorCode !== 0)
throw new Error('XML parse error: ' + doc.parseError.reason + ' line ' + doc.parseError.line + ' col ' + doc.parseError.linepos + '\nsrc = ' + doc.parseError.srcText);
} else {
// FF... others?
var root = doc.documentElement;
if (/^parsererror$/i.test(root.tagName))
throw new Error('XML parse error: ' + root.getInnerText());
}
return doc;
},
hasClass: function (node, className) {
if (!node)
return false;
return (' ' + node.className + ' ').contains(' ' + className + ' ');
},
setContent: function (node, content) {
if (content === null)
return node;
LAPI.DOM.removeChildren(node);
if (content.nodeName) { // presumably a DOM tree, like a span or a document fragment
node.appendChild(content);
} else if (node.innerHTML !== undefined) {
node.innerHTML = content.toString();
} else {
node.appendChild(document.createTextNode(content.toString()));
}
return node;
},
makeImage: function (src, width, height, title, doc) {
return LAPI.make(
'img', {
src: src,
width: String(width),
height: String(height),
title: title
}, doc);
},
makeButton: function (id, text, f, submit, doc) {
return LAPI.make(
'input', {
id: id || '',
type: (submit ? 'submit' : 'button'),
value: text,
onclick: f
}, doc);
},
makeLabel: function (id, text, for_elem, doc) {
var label = LAPI.make('label', {
id: id || '',
htmlFor: for_elem
}, null, doc);
return LAPI.DOM.setContent(label, text);
},
makeLink: function (url, text, tooltip, onclick, doc) {
var lk = LAPI.make('a', {
href: url,
title: tooltip,
onclick: onclick
}, null, doc);
return LAPI.DOM.setContent(lk, text || url);
},
// Unfortunately, extending Node.prototype may not work on some browsers,
// most notably (you've guessed it) IE...
getInnerText: function (node) {
if (node.textContent)
return node.textContent;
if (node.innerText)
return node.innerText;
var result = '';
if (node.nodeType === LAPI.DOM.TEXT_NODE) {
result = node.nodeValue;
} else {
Array.forEach(node.childNodes, function (elem) {
switch (elem.nodeType) {
case LAPI.DOM.ELEMENT_NODE:
result += LAPI.DOM.getInnerText(elem);
break;
case LAPI.DOM.TEXT_NODE:
result += elem.nodeValue;
break;
}
});
}
return result;
},
removeNode: function (node) {
if (node.parentNode)
node.parentNode.removeChild(node);
return node;
},
removeChildren: function (node) {
// if (typeof (node.innerHTML) !== 'undefined') node.innerHTML = "";
// Not a good idea. On IE this destroys all contained nodes, even if they're still referenced
// from JavaScript! Can't have that...
while (node.firstChild)
node.removeChild(node.firstChild);
return node;
},
insertNode: function (node, before) {
before.parentNode.insertBefore(node, before);
return node;
},
insertAfter: function (node, after) {
var next = after.nextSibling;
after.parentNode.insertBefore(node, next);
return node;
},
replaceNode: function (node, newNode) {
node.parentNode.replaceChild(node, newNode);
return newNode;
},
isParentOf: function (parent, child) {
while (child && child !== parent && child.parentNode)
child = child.parentNode;
return child === parent;
},
// Property is to be in CSS style, e.g. 'background-color', not in JS style ('backgroundColor')!
// Use standard 'cssFloat' for float property.
currentStyle: function (elem, property) {
function normalize(prop) {
// Don't use a regexp with a lambda function (available only in JS 1.3)... and I once had a
// case where IE6 goofed grossly with a lambda function. Since then I try to avoid those
// (though they're neat).
if (prop === 'cssFloat')
return 'styleFloat'; // We'll try both variants below, standard first...
var result = prop.split('-');
result =
Array.map(result, function (s) {
if (s)
return s.capitalizeFirst();
else
return s;
});
result = result.join('');
return result.lowercaseFirst();
}
if (elem.ownerDocument.defaultView && elem.ownerDocument.defaultView.getComputedStyle) { // Gecko etc.
if (property === 'cssFloat')
property = 'float';
return elem.ownerDocument.defaultView.getComputedStyle(elem, null).getPropertyValue(property);
} else {
var result;
if (elem.currentStyle) { // IE, has subtle differences to getComputedStyle
result = elem.currentStyle[property] || elem.currentStyle[normalize(property)];
} else { // Not exactly right, but best effort
result = elem.style[property] || elem.style[normalize(property)];
}
// Convert em etc. to pixels. Kudos to Dean Edwards; see
// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
if (!/^\d+(px)?$/i.test(result) && /^\d/.test(result) && elem.runtimeStyle) {
var style = elem.style.left,
runtimeStyle = elem.runtimeStyle.left;
elem.runtimeStyle.left = elem.currentStyle.left;
elem.style.left = result || 0;
result = elem.style.pixelLeft + 'px';
elem.style.left = style;
elem.runtimeStyle.left = runtimeStyle;
}
}
},
// Load a given image in a given size. Parameters:
// title
// Full title of the image, including the "File:" namespace
// url
// If !== null, URL of an existing thumb for that image. If width is null, may contain the url
// of the full image.
// width
// If !== null, desired width of the image, otherwise load the full image
// height
// If width !== null, height should also be set.
// auto_thumbs
// True if missing thumbnails are generated automatically.
// success
// Function to be called once the image is loaded. Takes one parameter: the IMG-tag of
// the loaded image
// failure
// Function to be called if the image cannot be loaded. Takes one parameter: a string
// containing an error message.
loadImage: function (title, url, width, height, auto_thumbs, success, failure) {
if (auto_thumbs && url) {
// MediaWiki-style with 404 handler. Set condition to false if your wiki does not have such a
// setup.
var img_src = null;
if (width) {
var i = url.lastIndexOf('/');
if (i >= 0)
img_src = url.substring(0, i) + url.substring(i).replace(/^\/\d+px-/, '/' + width + 'px-');
} else if (url) {
img_src = url;
}
if (!img_src) {
failure('Cannot load image from url ' + url);
return;
}
var img_loader =
LAPI.make(
'img', {
src: img_src
}, {
position: 'absolute',
top: '0px',
left: '0px',
display: 'none'
});
if (width)
img_loader.width = String(width);
if (height)
img_loader.height = String(height);
LAPI.Evt.attach(img_loader, 'load', function () {
success(img_loader);
});
document.body.appendChild(img_loader); // Now the browser goes loading the image
} else {
// No url to work with. Use parseWikitext to have a thumb generated an to get its URL.
LAPI.Ajax.parseWikitext(
'[[' + title + (width ? '|' + width + 'px' : '') + ']]',
function (html/* , failureFunc*/) {
var dummy =
LAPI.make(
'div', null, {
position: 'absolute',
top: '0px',
left: '0px',
display: 'none'
});
document.body.appendChild(dummy); // Now start loading the image
dummy.innerHTML = html;
var imgs = dummy.getElementsByTagName('img');
LAPI.Evt.attach(
imgs[0], 'load',
function () {
success(imgs[0]);
LAPI.DOM.removeNode(dummy);
});
},
function (request/* , json_result*/) {
failure('Image loading failed: ' + request.status + ' ' + request.statusText);
}, false // Not as preview
, null // user language: don't care
, null // on page: don't care
, 3600 // Cache for an hour
);
}
}
}; // end LAPI.DOM
LAPI.DOM.DocumentFacade = function () {
this.initialize.apply(this, arguments);
};
LAPI.DOM.DocumentFacade.prototype = {
initialize: function (text) {
// It's not a real document, but it will behave like one for our purposes.
this.documentElement = LAPI.make('div', null, {
display: 'none',
position: 'absolute'
});
this.body = LAPI.make('div', null, {
position: 'relative'
});
this.documentElement.appendChild(this.body);
document.body.appendChild(this.documentElement);
this.body.innerHTML = text;
// Find all forms
var forms = document.getElementsByTagName('form'),
self = this;
this.forms = Array.select(forms, function (f) {
return LAPI.DOM.isParentOf(self.body, f);
});
// Konqueror 4.2.3/4.2.4 clears form.elements when the containing div is removed from the
// parent document?!
if (!LAPI.Browser.is_khtml) {
LAPI.DOM.removeNode(this.documentElement);
} else {
this.dispose = function () {
LAPI.DOM.removeNode(this.documentElement);
};
// Since we must leave the stuff *in* the original document on Konqueror, we'll also need a
// dispose routine... what an ugly hack.
}
this.allIDs = {};
this.isFake = true;
},
createElement: function (tag) {
return document.createElement(tag);
},
createDocumentFragment: function () {
return document.createDocumentFragment();
},
createTextNode: function (text) {
return document.createTextNode(text);
},
createComment: function (text) {
return document.createComment(text);
},
createCDATASection: function (text) {
return document.createCDATASection(text);
},
createAttribute: function (name) {
return document.createAttribute(name);
},
createEntityReference: function (name) {
return document.createEntityReference(name);
},
createProcessingInstruction: function (target, data) {
return document.createProcessingInstruction(target, data);
},
getElementsByTagName: function (tag) {
// Grossly inefficient, but deprecated anyway
var res = [];
function traverse(node, tag) {
if (node.nodeName.toLowerCase() === tag)
res[res.length] = node;
var curr = node.firstChild;
while (curr) {
traverse(curr, tag);
curr = curr.nextSibling;
}
}
traverse(this.body, tag.toLowerCase());
return res;
},
getElementById: function (id) {
function traverse(elem, id) {
if (elem.id === id)
return elem;
var res = null,
curr = elem.firstChild;
while (curr && !res) {
res = traverse(curr, id);
curr = curr.nextSibling;
}
return res;
}
if (!this.allIDs[id])
this.allIDs[id] = traverse(this.body, id);
return this.allIDs[id];
}
// ...NS operations omitted
}; // end DocumentFacade
if (document.importNode) {
LAPI.DOM.DocumentFacade.prototype.importNode = function (node, deep) {
document.importNode(node, deep);
};
}
} // end if (guard)
if (!LAPI.WP) {
LAPI.WP = {
getContentDiv: function (doc) {
// Monobook, modern, classic skins
return LAPI.$(['bodyContent', 'mw_contentholder', 'article'], doc);
},
fullImageSizeFromPage: function (doc) {
// Get the full img size. This is screenscraping :-( but there are times where you don't
// want to get this info from the server using an Ajax call.
// Note: we get the size from the file history table because the text just below the image
// is all scrambled on RTL wikis. For instance, on ar-WP, it is
// "\u200f (1,806 × 1,341 بكسل، حجم الملف: 996 كيلوبايت، نوع الملف: image/jpeg) and with uselang=en,
// it is at ar-WP "\u200f (1,806 × 1,341 pixels, file size: 996 KB, MIME type: image/jpeg)"
// However, in the file history table, it looks good no matter the language and writing
// direction.
// Update: this fails on e.g. ar-WP because someone had the great idea to use localized
// numerals, but the digit transform table is empty!
var result = {
width: 0,
height: 0
},
file_hist = LAPI.$('mw-imagepage-section-filehistory', doc);
if (!file_hist)
return result;
try {
var $file_curr = $ ? $(file_hist).find('td.filehistory-selected') : document.getElementsByClassName(file_hist, 'td', 'filehistory-selected');
// Did they change the column order here? It once was nextSibling.nextSibling... but somehow
// the thumbnails seem to be gone... Right:
// http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/ImagePage.php?r1=52385&r2=53130
file_hist = LAPI.DOM.getInnerText($file_curr[0].nextSibling);
if (!file_hist.contains('×')) {
file_hist = LAPI.DOM.getInnerText($file_curr[0].nextSibling.nextSibling);
if (!file_hist.contains('×'))
file_hist = null;
}
} catch (ex) {
return result;
}
// Now we have "number×number" followed by something arbitrary
if (file_hist) {
file_hist = file_hist.split('×', 2);
result.width = parseInt(file_hist.shift().replace(/[^0-9]/g, ''), 10);
// Height is a bit more difficult because e.g. uselang=eo uses a space as the thousands
// separator. Hence we have to extract this more carefully
file_hist = file_hist.pop(); // Everything after the "×"
// Remove any white space embedded between digits
file_hist = file_hist.replace(/(\d)\s*(\d)/g, '$1$2');
file_hist = file_hist.split(' ', 2).shift().replace(/[^0-9]/g, '');
result.height = parseInt(file_hist, 10);
if (isNaN(result.width) || isNaN(result.height)) {
result = {
width: 0,
height: 0
};
}
}
return result;
},
getPreviewImage: function (title, doc) {
var file_div = LAPI.$('file', doc);
if (!file_div)
return null; // Catch page without file...
var imgs = file_div.getElementsByTagName('img');
title = title || mw.config.get('wgTitle');
for (var i = 0; i < imgs.length; i++) {
var src = imgs[i].getAttribute('src', 2);
if (src && src.search(/^data/)) {
src = decodeURIComponent(src).replace('%26', '&');
if (!src.search(new RegExp('^' + LAPI_file_store + '.*/' + title.replace(/ /g, '_').escapeRE() + '(/.*)?$')))
return imgs[i];
}
}
return null;
},
pageFromLink: function (lk) {
if (!lk)
return null;
var href = lk.getAttribute('href', 2);
if (!href)
return null;
// This is a bit tricky to get right, because 'wgScript' can be a substring prefix of
// article path, or vice versa.
var script = mw.config.get('wgScript') + '?';
if (href.startsWith(script) || href.startsWith(mw.config.get('wgServer') + script) || mw.config.get('wgServer').startsWith('//') && href.startsWith(document.location.protocol + mw.config.get('wgServer') + script)) {
// href="/w/index.php?title=..."
return href.getParamValue('title');
}
// Now try article path: href="/wiki/..."
var prefix = mw.config.get('wgArticlePath').replace('$1', '');
if (!href.startsWith(prefix))
prefix = mw.config.get('wgServer') + prefix; // Fully expanded URL?
if (!href.startsWith(prefix) && prefix.startsWith('//'))
prefix = document.location.protocol + prefix; // Protocol-relative 'wgServer'?
if (href.startsWith(prefix))
return decodeURIComponent(href.substring(prefix.length));
// Do we have variants?
var variants = mw.config.get('wgVariantArticlePath');
if (variants && variants.length > 0) {
var re =
new RegExp(variants.escapeRE().replace('\\$2', '[^\\/]*').replace('\\$1', '(.*)')),
m = re.exec(href);
if (m && m.length > 1)
return decodeURIComponent(m[m.length - 1]);
}
// Finally alternative action paths
var actions = mw.config.get('wgActionPaths');
if (actions) {
for (var i = 0; i < actions.length; i++) {
var p = actions[i];
if (p && p.length > 0) {
p = p.replace('$1', '');
if (!href.startsWith(p))
p = mw.config.get('wgServer') + p;
if (!href.startsWith(p) && p.startsWith('//'))
p = document.location.protocol + p;
if (href.startsWith(p))
return decodeURIComponent(href.substring(p.length));
}
}
}
return null;
},
revisionFromHtml: function (htmlOfPage) {
var revision_id = null;
if (window.RLCONF) // MW 1.32+
revision_id = htmlOfPage.match(/RLCONF=\{.*"wgCurRevisionId"\s*:\s*(\d+),/);
else if (window.mediaWiki) // MW 1.17+
revision_id = htmlOfPage.match(/(?:mediaWiki|mw).config.set\(\{.*"wgCurRevisionId"\s*:\s*(\d+),/);
else // MW < 1.17
revision_id = htmlOfPage.match(/wgCurRevisionId\s*=\s*(\d+)[;,]/);
if (revision_id)
revision_id = parseInt(revision_id[1], 10);
return revision_id;
}
}; // end LAPI.WP
} // end if (guard)
if (!LAPI.Ajax.doAction) {
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
// <textarea> 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 <textarea> start tag if the value starts with a newline. That
// solves bug 12130 (leading newlines swallowed), but since XML passes us this extra newline, we might end up adding a
// leading newline upon each edit.
// Let's try to make sure that all textarea's values are as they should be in HTML.
// Note: since the above change to always use our own parser, which always returns a faked HTML document, this should be
// unnecessary since doc.isFake should always be true.
if (!LAPI.Ajax.getHTML.extraNewlineRE) {
// Feature detection. Compare value after parsing with value after .innerHTML.
LAPI.Ajax.getHTML.extraNewlineRE = null; // Don't know; hence do nothing
try {
var testTA = '<textarea id="test">\nTest</textarea>',
testString = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n' +
'<html xmlns="http://www.w3.org/1999/xhtml" lang="en" dir="ltr">\n' +
'<head><title>Test</title></head><body><form>' + testTA +
'</form></body>\n</html>',
testDoc = LAPI.DOM.parseHTML(testString, 'test'),
testVal = String(testDoc.getElementById('test').value);
if (testDoc.dispose)
testDoc.dispose();
var testDiv = LAPI.make('div', null, {
display: 'none'
});
document.body.appendChild(testDiv);
testDiv.innerHTML = testTA;
if (testDiv.firstChild.value !== testVal) {
LAPI.Ajax.getHTML.extraNewlineRE = /^\r?\n/;
if (testDiv.firstChild.value !== testVal.replace(LAPI.Ajax.getHTML.extraNewlineRE, '')) {
// Huh? Not the expected difference: go back to "don't know" mode
LAPI.Ajax.getHTML.extraNewlineRE = null;
}
}
LAPI.DOM.removeNode(testDiv);
} catch (any) {
LAPI.Ajax.getHTML.extraNewlineRE = null;
}
}
if (!doc.isFake && LAPI.Ajax.getHTML.extraNewlineRE !== null) {
// If have a "fake" doc, then we did parse through .innerHTML anyway. No need to fix anything.
// (Hm. Maybe we should just always use a fake doc?)
var tas = doc.getElementsByTagName('textarea');
for (var i = 0, l = tas.length; i < l; i++)
tas[i].value = tas[i].value.replace(LAPI.Ajax.getHTML.extraNewlineRE, '');
}
return doc;
};
LAPI.Ajax.get = function (uri, params, success, failure, config) {
var original_failure = failure;
if (!failure || typeof failure !== 'function')
failure = function () {};
if (!success || typeof success !== 'function')
throw new Error('No success function supplied for LAPI.Ajax.get ' + uri + ' with arguments ' + params.toString());
var request = LAPI.Ajax.getRequest();
if (!request) {
failure(request);
return;
}
var args = '',
question_mark = uri.indexOf('?');
if (question_mark) {
args = uri.substring(question_mark + 1);
uri = uri.substring(0, question_mark);
}
if (params !== null) {
if (typeof params === 'string' && params.length > 0) {
args += (args.length > 0 ? '&' : '') + ((params[0] === '&' || params[0] === '?') ? params.substring(1) : params); // Must already be encoded!
} else {
for (var param in params) {
args += (args.length > 0 ? '&' : '') + param;
if (params[param] !== null)
args += '=' + encodeURIComponent(params[param]);
}
}
}
var method;
if (uri.startsWith('//'))
uri = document.location.protocol + uri; // Avoid protocol-relative URIs (IE7 bug)
if (uri.length + args.length + 1 < (LAPI.Browser.is_ie ? 2040 : 4080)) {
// Both browsers and web servers may have limits on URL length. IE has a limit of 2083 characters
// (2048 in the path part), and the WMF servers seem to impose a limit of 4kB.
method = 'GET';
uri += '?' + args;
args = null;
} else {
method = 'POST'; // We'll lose caching, but at least we can make the request.
}
request.open(method, uri, true);
request.setRequestHeader('Pragma', 'cache=yes');
request.setRequestHeader(
'Cache-Control', 'no-transform' + (params && params.maxage ? ', max-age=' + params.maxage : '') + (params && params.smaxage ? ', s-maxage=' + params.smaxage : ''));
if (config) {
for (var conf in config) {
if (conf === 'overrideMimeType') {
if (config[conf] && config[conf].length > 0 && request.overrideMimeType)
request.overrideMimeType(config[conf]);
} else {
request.setRequestHeader(conf, config[conf]);
}
}
}
if (args)
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
request.onreadystatechange = function () {
if (request.readyState !== 4)
return; // Wait until the request has completed.
try {
if (request.status !== 200)
throw new Error('#Request to server failed. Status: ' + request.status + ' ' + request.statusText + ' URI: ' + uri);
if (!request.responseText)
throw new Error('#Empty response from server for request ' + uri);
} catch (ex) {
failure(request, ex);
return;
}
success(request, original_failure);
};
request.send(args);
};
LAPI.Ajax.getPage = function (page, action, params, success, failure) {
var uri = mw.config.get('wgServer') + mw.config.get('wgScript') + '?title=' + encodeURIComponent(page) + (action ? '&action=' + action : '');
LAPI.Ajax.get(uri, params, success, failure, {
overrideMimeType: 'application/xml'
});
};
// modify is supposed to save the changes at the end, e.g. using LAPI.Ajax.submit.
// modify is called with three parameters: the document, possibly the form, and the optional
// failure function. The failure function is called with the request as the first parameter,
// and possibly an exception as the second parameter.
LAPI.Ajax.doAction = function (page, action, form, modify, failure) {
if (!page || !action || !modify || typeof modify !== 'function')
throw new Error('Parameter inconsistency in LAPI.Ajax.doAction.');
var original_failure = failure;
if (!failure || typeof failure !== 'function')
failure = function () {};
LAPI.Ajax.getPage(
page, action, null, // No additional parameters
function (request, failureFunc) {
var doc = null,
the_form = null,
revision_id = null;
try {
// Convert responseText into DOM tree.
doc = LAPI.Ajax.getHTML(request, failureFunc, form);
if (!doc)
return;
var err_msg = LAPI.$('permissions-errors', doc);
if (err_msg)
throw new Error('#' + LAPI.DOM.getInnerText(err_msg));
if (form) {
the_form = LAPI.$(form, doc);
if (!the_form)
throw new Error('#Server reply does not contain mandatory form.');
the_form.wpWatchthis.checked = !!document.getElementById('ca-unwatch');
}
revision_id = LAPI.WP.revisionFromHtml(request.responseText);
} catch (ex) {
failureFunc(request, ex);
return;
}
modify(doc, the_form, original_failure, revision_id);
}, failure);
}; // end LAPI.Ajax.doAction
LAPI.Ajax.submit = function (form, after_submit) {
try {
ajaxSubmit(form, null, after_submit, true); // Legacy code from MediaWiki:AjaxSubmit
} catch (ex) {
after_submit(null, ex);
}
}; // end LAPI.Ajax.submit
LAPI.Ajax.editPage = function (page, modify, failure) {
LAPI.Ajax.doAction(page, 'edit', 'editform', modify, failure);
}; // end LAPI.Ajax.editPage
LAPI.Ajax.checkEdit = function (request) {
if (!request)
return true;
// Check for previews (session token lost?) or edit forms (edit conflict).
try {
var doc = LAPI.Ajax.getHTML(request, function () {
throw new Error('Cannot check HTML');
});
if (!doc)
return false;
return LAPI.$(['wikiPreview', 'editform'], doc) === null;
} catch (anything) {
return false;
}
}; // end LAPI.Ajax.checkEdit
LAPI.Ajax.submitEdit = function (form, success, failure) {
if (!success || typeof success !== 'function')
success = function () {};
if (!failure || typeof failure !== 'function')
failure = function () {};
LAPI.Ajax.submit(
form,
function (request, ex) {
if (ex) {
failure(request, ex);
} else {
var successful = false;
try {
successful = request && request.status === 200 && LAPI.Ajax.checkEdit(request);
} catch (some_error) {
failure(request, some_error);
return;
}
if (successful)
success(request);
else
failure(request);
}
});
}; // end LAPI.Ajax.submitEdit
LAPI.Ajax.apiGet = function (action, params, success, failure) {
var original_failure = failure;
if (!failure || typeof failure !== 'function')
failure = function () {};
if (!success || typeof success !== 'function')
throw new Error('No success function supplied for LAPI.Ajax.apiGet ' + action + ' with arguments ' + params.toString());
var is_json = false;
if (params !== null) {
if (typeof params === 'string') {
if (!/format=[^&]+/.test(params))
params += '&format=json';
is_json = /format=json(&|$)/.test(params); // Exclude jsonfm, which actually serves XHTML
} else {
if (typeof params.format !== 'string' || !params.format.length)
params.format = 'json';
is_json = params.format === 'json';
}
}
var uri = mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php' + (action ? '?action=' + action : '');
LAPI.Ajax.get(
uri, params,
function (request, failureFunc) {
if (is_json && request.responseText.trimLeft()[0] !== '{') {
failureFunc(request);
} else {
var json;
try {
json = (is_json ? eval('(' + request.responseText.trimLeft() + ')') : null)
} catch (e) {
json = null;
}
success(
request, json, original_failure);
}
}, failure);
}; // end LAPI.Ajax.apiGet
LAPI.Ajax.parseWikitext = function (wikitext, success, failure, as_preview, user_language, on_page, cache) {
if (!failure || typeof failure !== 'function')
failure = function () {};
if (!success || typeof success !== 'function')
throw new Error('No success function supplied for parseWikitext');
if (!wikitext && !on_page)
throw new Error('No wikitext or page supplied for parseWikitext');
var params = null;
if (!wikitext) {
params = {
pst: null,
page: on_page
};
} else {
params = {
pst: null, // Do the pre-save-transform: Pipe magic, tilde expansion, etc.
text: (as_preview ? '<div style="border:1px solid red; padding:0.5em;"><div class="previewnote">\{\{MediaWiki:Previewnote/' +
(user_language || mw.config.get('wgUserLanguage')) + '\}\}</div><div>\n' : '') + wikitext +
(as_preview ? '</div><div style="clear:both;"></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 (!window.jQuery || !window.mediaWiki || !mw.loader) {
// Assume old-stlye
if (window.injectSpinner)
LAPI.Ajax.injectSpinner = window.injectSpinner;
if (window.removeSpinner)
LAPI.Ajax.removeSpinner = window.removeSpinner;
} else {
mw.loader.using('jquery.spinner', function () {
LAPI.Ajax.injectSpinner = function (elementBefore, id) {
$(elementBefore).injectSpinner(id);
};
LAPI.Ajax.removeSpinner = function (id) {
$.removeSpinner(id);
};
});
}
} // end if (guard)
if (!LAPI.Pos) {
LAPI.Pos = {
// Returns the global coordinates of the mouse pointer within the document.
mousePosition: function (evt) {
if (!evt || (!evt.pageX && !evt.clientX))
// No way to calculate a mouse pointer position
return null;
if (evt.pageX.evt) {
return {
x: evt.pageX,
y: evt.pageY
};
}
var offset = LAPI.Pos.scrollOffset(),
mouse_delta = LAPI.Pos.mouse_offset(),
coor_x = evt.clientX + offset.x - mouse_delta.x,
coor_y = evt.clientY + offset.y - mouse_delta.y;
return {
x: coor_x,
y: coor_y
};
},
// Operations on document level:
// Returns the scroll offset of the whole document (in other words, the coordinates
// of the top left corner of the viewport).
scrollOffset: function () {
return {
x: LAPI.Pos.getScroll('Left'),
y: LAPI.Pos.getScroll('Top')
};
},
getScroll: function (what) {
var s = 'scroll' + what;
return (document.documentElement ? document.documentElement[s] : 0) || document.body[s] || 0;
},
// Returns the size of the viewport (result.x is the width, result.y the height).
viewport: function () {
return {
x: LAPI.Pos.getViewport('Width'),
y: LAPI.Pos.getViewport('Height')
};
},
getViewport: function (what) {
if (LAPI.Browser.is_opera_95 && what === 'Height' || LAPI.Browser.is_safari && !document.evaluate)
return window['inner' + what];
var s = 'client' + what;
if (LAPI.Browser.is_opera)
return document.body[s];
return (document.documentElement ? document.documentElement[s] : 0) || document.body[s] || 0;
},
// Operations on DOM nodes
position: (function () {
// The following is the jQuery.offset implementation. We cannot use jQuery yet in globally
// activated scripts (it has strange side effects for Opera 8 users who can't log in anymore,
// and it breaks the search box for some users). Note that jQuery does not support Opera 8.
// Until the WMF servers serve jQuery by default, this copy from the jQuery 1.3.2 sources is
// needed here. If and when we have jQuery available officially, the whole thing here can be
// replaced by "var tmp = jQuery (node).offset(); return {x:tmp.left, y:tmp.top};"
// Kudos to the jQuery development team. Any errors in this adaptation are my own. (Lupo,
// 2009-08-24).
var data = null;
function jQuery_init() {
data = {};
// Capability check from jQuery.
var body = document.body,
container = document.createElement('div'),
html =
'<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;' +
'padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;' +
'top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;" ' +
'cellpadding="0" cellspacing="0"><tr><td></td></tr></table>',
rules = {
position: 'absolute',
visibility: 'hidden',
top: 0,
left: 0,
margin: 0,
border: 0,
width: '1px',
height: '1px'
};
Object.merge(rules, container.style);
container.innerHTML = html;
body.insertBefore(container, body.firstChild);
var innerDiv = container.firstChild,
checkDiv = innerDiv.firstChild,
td = innerDiv.nextSibling.firstChild.firstChild;
data.doesNotAddBorder = (checkDiv.offsetTop !== 5);
data.doesAddBorderForTableAndCells = (td.offsetTop === 5);
innerDiv.style.overflow = 'hidden';
innerDiv.style.position = 'relative';
data.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5);
var bodyMarginTop = body.style.marginTop;
body.style.marginTop = '1px';
data.doesNotIncludeMarginInBodyOffset = (body.offsetTop === 0);
body.style.marginTop = bodyMarginTop;
body.removeChild(container);
}
function jQuery_offset(node) {
if (node === node.ownerDocument.body)
return jQuery_bodyOffset(node);
if (node.getBoundingClientRect) {
var box = node.getBoundingClientRect(),
scroll = LAPI.Pos.scrollOffset();
return {
x: (box.left + scroll.x),
y: (box.top + scroll.y)
};
}
if (!data)
jQuery_init();
var elem = node,
offsetParent = elem.offsetParent,
// prevOffsetParent = elem,
doc = elem.ownerDocument,
prevComputedStyle = doc.defaultView.getComputedStyle(elem, null),
computedStyle,
top = elem.offsetTop,
left = elem.offsetLeft;
while ((elem = elem.parentNode) && elem !== doc.body && elem !== doc.documentElement) {
computedStyle = doc.defaultView.getComputedStyle(elem, null);
top -= elem.scrollTop;
left -= elem.scrollLeft;
if (elem === offsetParent) {
top += elem.offsetTop;
left += elem.offsetLeft;
if (data.doesNotAddBorder && !(data.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.tagName))) {
top += parseInt(computedStyle.borderTopWidth, 10) || 0;
left += parseInt(computedStyle.borderLeftWidth, 10) || 0;
}
// prevOffsetParent = offsetParent;
offsetParent = elem.offsetParent;
}
if (data.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== 'visible') {
top += parseInt(computedStyle.borderTopWidth, 10) || 0;
left += parseInt(computedStyle.borderLeftWidth, 10) || 0;
}
prevComputedStyle = computedStyle;
}
if (prevComputedStyle.position === 'relative' || prevComputedStyle.position === 'static') {
top += doc.body.offsetTop;
left += doc.body.offsetLeft;
}
if (prevComputedStyle.position === 'fixed') {
top += Math.max(doc.documentElement.scrollTop, doc.body.scrollTop);
left += Math.max(doc.documentElement.scrollLeft, doc.body.scrollLeft);
}
return {
x: left,
y: top
};
}
function jQuery_bodyOffset(body) {
if (!data)
jQuery_init();
var top = body.offsetTop,
left = body.offsetLeft;
if (data.doesNotIncludeMarginInBodyOffset) {
top += parseInt(LAPI.DOM.currentStyle(body, 'margin-top'), 10) || 0;
left += parseInt(LAPI.DOM.currentStyle(body, 'margin-left'), 10) || 0;
}
return {
x: left,
y: top
};
}
return jQuery_offset;
}()),
isWithin: function (node, x, y) {
if (!node || !node.parentNode)
return false;
var pos = LAPI.Pos.position(node);
return (x === null || x > pos.x && x < pos.x + node.offsetWidth) &&
(y === null || y > pos.y && y < pos.y + node.offsetHeight);
},
// Private:
// IE has some strange offset...
mouse_offset: function () {
if (LAPI.Browser.is_ie) {
var doc_elem = document.documentElement;
if (doc_elem) {
if (typeof doc_elem.getBoundingClientRect === 'function') {
var tmp = doc_elem.getBoundingClientRect();
return {
x: tmp.left,
y: tmp.top
};
} else {
return {
x: doc_elem.clientLeft,
y: doc_elem.clientTop
};
}
}
}
return {
x: 0,
y: 0
};
}
}; // end LAPI.Pos
} // end if (guard)
if (!LAPI.Evt) {
LAPI.Evt = {
listenTo: function (object, node, evt, f, capture) {
var listener = LAPI.Evt.makeListener(object, f);
LAPI.Evt.attach(node, evt, listener, capture);
},
attach: function (node, evt, f, capture) {
if (node.attachEvent)
node.attachEvent('on' + evt, f);
else if (node.addEventListener)
node.addEventListener(evt, f, capture);
else
node['on' + evt] = f;
},
remove: function (node, evt, f, capture) {
if (node.detachEvent)
node.detachEvent('on' + evt, f);
else if (node.removeEventListener)
node.removeEventListener(evt, f, capture);
else
node['on' + evt] = null;
},
makeListener: function (obj, listener) {
// Some hacking around to make sure 'this' is set correctly
var object = obj,
f = listener;
return function (evt) {
return f.apply(object, [evt || window.event]);
};
// Alternative implementation:
// var f = listener.bind (obj);
// return function (evt) { return f (evt || window.event); };
},
kill: function (evt) {
if (typeof evt.preventDefault === 'function') {
evt.stopPropagation();
evt.preventDefault(); // Don't follow the link
} else if (evt.cancelBubble.evt) { // IE...
evt.cancelBubble = true;
}
return false; // Don't follow the link (IE)
}
}; // end LAPI.Evt
} // end if (guard)
if (!LAPI.Edit) {
LAPI.Edit = function () {
this.initialize.apply(this, arguments);
};
LAPI.Edit.SAVE = 1;
LAPI.Edit.PREVIEW = 2;
LAPI.Edit.REVERT = 4;
LAPI.Edit.CANCEL = 8;
LAPI.Edit.prototype = {
initialize: function (initial_text, columns, rows, labels, handlers) {
var my_labels = {
box: null,
preview: null,
save: 'Save',
cancel: 'Cancel',
nullsave: null,
revert: null,
post: null
};
if (labels)
my_labels = Object.merge(labels, my_labels);
this.labels = my_labels;
this.timestamp = (new Date()).getTime();
this.id = 'simpleedit_' + this.timestamp;
this.view = LAPI.make('div', {
id: this.id
}, {
marginRight: '1em'
});
// Somehow, the textbox extends beyond the bounding box of the view. Don't know why, but
// adding a small margin fixes the layout more or less.
this.form =
LAPI.make(
'form', {
id: this.id + '_form',
action: '',
onsubmit: (function () {})
});
if (my_labels.box) {
var label = LAPI.make('div');
label.appendChild(LAPI.DOM.makeLabel(this.id + '_label', my_labels.box, this.id + '_text'));
this.form.appendChild(label);
}
this.textarea =
LAPI.make(
'textarea', {
id: this.id + '_text',
cols: columns,
rows: rows,
value: (initial_text ? initial_text.toString() : '')
});
LAPI.Evt.attach(this.textarea, 'keyup', LAPI.Evt.makeListener(this, this.text_changed));
// Catch cut/copy/paste through the context menu. Some browsers support oncut, oncopy,
// onpaste events for this, but since that's only IE, FF 3, Safari 3, and Chrome, we
// cannot rely on this. Instead, we check again as soon as we leave the textarea. Only
// minor catch is that on FF 3, the next focus target is determined before the blur event
// fires. Since in practice save will always be enabled, this shouldn't be a problem.
LAPI.Evt.attach(this.textarea, 'mouseout', LAPI.Evt.makeListener(this, this.text_changed));
LAPI.Evt.attach(this.textarea, 'blur', LAPI.Evt.makeListener(this, this.text_changed));
this.form.appendChild(this.textarea);
this.form.appendChild(LAPI.make('br'));
this.preview_section =
LAPI.make('div', null, {
borderBottom: '1px solid #88a',
display: 'none'
});
this.view.insertBefore(this.preview_section, this.view.firstChild);
this.save =
LAPI.DOM.makeButton(this.id + '_save', my_labels.save, LAPI.Evt.makeListener(this, this.do_save));
this.form.appendChild(this.save);
if (my_labels.preview) {
this.preview =
LAPI.DOM.makeButton(this.id + '_preview', my_labels.preview, LAPI.Evt.makeListener(this, this.do_preview));
this.form.appendChild(this.preview);
}
this.cancel =
LAPI.DOM.makeButton(this.id + '_cancel', my_labels.cancel, LAPI.Evt.makeListener(this, this.do_cancel));
this.form.appendChild(this.cancel);
this.view.appendChild(this.form);
if (my_labels.post) {
this.post_text = LAPI.DOM.setContent(LAPI.make('div'), my_labels.post);
this.view.appendChild(this.post_text);
}
if (handlers)
Object.merge(handlers, this);
if (typeof this.ongettext !== 'function') {
this.ongettext = function (text) {
return text;
};
} // Default: no modifications
this.current_mask = LAPI.Edit.SAVE + LAPI.Edit.PREVIEW + LAPI.Edit.REVERT + LAPI.Edit.CANCEL;
if ((!initial_text || !initial_text.trim().length) && this.preview)
this.preview.disabled = true;
if (my_labels.revert) {
this.revert =
LAPI.DOM.makeButton(this.id + '_revert', my_labels.revert, LAPI.Evt.makeListener(this, this.do_revert));
this.form.insertBefore(this.revert, this.cancel);
}
this.original_text = '';
},
getView: function () {
return this.view;
},
getText: function () {
return this.ongettext(this.textarea.value);
},
setText: function (text) {
this.textarea.value = text;
this.original_text = text;
this.text_changed();
},
changeText: function (text) {
this.textarea.value = text;
this.text_changed();
},
hidePreview: function () {
this.preview_section.style.display = 'none';
if (this.onpreview)
this.onpreview(this);
},
showPreview: function () {
this.preview_section.style.display = '';
if (this.onpreview)
this.onpreview(this);
},
setPreview: function (html) {
if (html.nodeName) {
LAPI.DOM.removeChildren(this.preview_section);
this.preview_section.appendChild(html);
} else {
this.preview_section.innerHTML = html;
}
},
busy: function (show) {
if (show)
LAPI.Ajax.injectSpinner(this.cancel, this.id + '_spinner');
else
LAPI.Ajax.removeSpinner(this.id + '_spinner');
},
do_save: function (/* evt*/) {
if (this.onsave)
this.onsave(this);
return true;
},
do_revert: function (/* evt*/) {
this.changeText(this.original_text);
return true;
},
do_cancel: function (/* evt*/) {
if (this.oncancel)
this.oncancel(this);
return true;
},
do_preview: function (/* evt*/) {
var self = this;
this.busy(true);
LAPI.Ajax.parseWikitext(
this.getText(),
function (text/* , failureFunc*/) {
self.busy(false);
self.setPreview(text);
self.showPreview();
},
function (/* req, json_result*/) {
// Error. TODO: user feedback?
self.busy(false);
}, true, mw.config.get('wgUserLanguage') || null, mw.config.get('wgPageName') || null);
return true;
},
enable: function (bit_set) {
var call_text_changed = false;
this.current_mask = bit_set;
this.save.disabled = ((bit_set & LAPI.Edit.SAVE) === 0);
this.cancel.disabled = ((bit_set & LAPI.Edit.CANCEL) === 0);
if (this.preview) {
if ((bit_set & LAPI.Edit.PREVIEW) === 0)
this.preview.disabled = true;
else
call_text_changed = true;
}
if (this.revert) {
if ((bit_set & LAPI.Edit.REVERT) === 0)
this.revert.disabled = true;
else
call_text_changed = true;
}
if (call_text_changed)
this.text_changed();
},
text_changed: function (/* evt*/) {
var text = this.textarea.value;
text = text.trim();
var length = text.length;
if (this.preview && (this.current_mask & LAPI.Edit.PREVIEW) !== 0) {
// Preview is basically enabled
this.preview.disabled = (length <= 0);
}
if (this.labels.nullsave)
this.save.value = (length > 0) ? this.labels.save : this.labels.nullsave;
if (this.revert) {
this.revert.disabled =
(text === this.original_text || this.textarea.value === this.original_text);
}
return true;
}
}; // end LAPI.Edit
} // end if (guard)
// </nowiki>