User:Lupo/lapi2.js
Jump to navigation
Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
Documentation for this user script can be added at User:Lupo/lapi2. |
// <source lang=javascript">
/*
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, some tailored to MediaWiki/WMF sites, including a cross-browser
DOM parser
- LAPI.Edit Simple editor implementation with save, cancel, preview (for WMF sites)
- LAPI.Evt Event handler routines (general)
- LAPI.Pos Position calculations (general)
*/
// Global: wgServer, wgScript, wgUserLanguage, injectSpinner, removeSpinner (from wiki.js)
// 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 (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 (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 (typeof (predicate) == 'undefined')
return Object.merge (from, into, includeInherited);
for (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 all whitespace from both ends of the string.
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace (/^\s+|\s+$/g, "");
};
}
// Removes all whitespace from the beginning of the string.
if (!String.prototype.trimLeft) {
String.prototype.trimLeft = function () {
return this.replace (/^\s\s*/, "");
};
}
String.prototype.trimFront = String.prototype.trimLeft; // Synonym
// Removes all whitespace from the end of the string.
if (!String.prototype.trimRight) {
String.prototype.trimRight = function () {
return this.replace (/\s\s*$/, "");
};
}
String.prototype.trimEnd = String.prototype.trimRight; // Synonym
/** Further String enhancements ************/
// Returns true if the string begins with prefix.
String.prototype.startsWith = function (prefix) {
return this.indexOf (prefix) == 0;
};
// Returns true if the string ends in suffix
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);
};
// 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 () + '=([^&]*)');
var m = re.exec (this);
if (m && m.length >= 2) return decodeURIComponent (m[1]);
return null;
};
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 ************/
// Return a function that calls the function with 'this' bound to 'thisObject'
Function.prototype.bind = function (thisObject) {
var f = this, obj = thisObject;
return function () { return f.apply (obj, arguments); };
};
/** Array enhancements (Javascript 1.6) ************/
// Note that contrary to JS 1.6, we treat the thisObject as optional.
// Returns a new array containing only those elements for which predicate
// is true.
if (!Array.prototype.filter) {
Array.prototype.filter = function (predicate, thisObject)
{
return Array.filter (this, predicate, thisObject);
};
}
if (!Array.filter) {
Array.filter = function (target, 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;
};
}
Array.prototype.select = Array.prototype.filter; // Synonym
Array.select = Array.filter; // Synonym
// Calls iterator on all elements of the array
if (!Array.prototype.forEach) {
Array.prototype.forEach = function (iterator, thisObject)
{
Array.forEach (this, iterator, thisObject);
};
}
if (!Array.forEach) {
Array.forEach = function (target, iterator, thisObject)
{
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.prototype.every) {
Array.prototype.every = function (predicate, thisObject)
{
return Array.every (this, predicate, thisObject);
};
}
if (!Array.every) {
Array.every = function (target, 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;
};
}
Array.prototype.forAll = Array.prototype.every; // Synonym
Array.forAll = Array.every; // Synonym
// Returns true if predicate is true for at least one element of the array, false otherwise.
if (!Array.prototype.some) {
Array.prototype.some = function (predicate, thisObject)
{
return Array.some (this, predicate, thisObject);
};
}
if (!Array.some) {
Array.some = function (target, 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;
};
}
Array.prototype.exists = Array.prototype.some; // Synonym
Array.exists = Array.some; // Synonym
// Returns a new array built by applying mapper to all elements.
if (!Array.prototype.map) {
Array.prototype.map = function (mapper, thisObject)
{
return Array.map (this, mapper, thisObject);
};
}
if (!Array.map) {
Array.map = function (target, 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.prototype.indexOf) {
Array.prototype.indexOf = function (elem, from)
{
return Array.indexOf (this, elem, from);
};
}
if (!Array.indexOf) {
Array.indexOf = function (target, 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.prototype.lastIndexOf) {
Array.prototype.lastIndexOf = function (elem, from)
{
return Array.lastIndexOf (this, elem, from);
};
}
if (!Array.lastIndexOf) {
Array.lastIndexOf = function (target, 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 ************/
Array.prototype.remove = function (elem) {
var i = this.indexOf (elem);
if (i >= 0) this.splice (i, 1);
};
Array.prototype.contains = function (elem) {
return this.indexOf (elem) >= 0;
};
Array.prototype.flatten = function () {
var result = [];
this.forEach (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.prototype.any = function (selector, thisObject)
{
return Array.any (this, selector, thisObject);
};
Array.any = function (target, selector, thisObject)
{
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".
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 (LAPI) == 'undefined') {
var LAPI = {
Ajax :
{
getRequest : function ()
{
var request = null;
try {
request = new XMLHttpRequest();
} catch (anything) {
if (!!window.ActiveXObject) {
try {
request = new ActiveXObject("Msxml2.XMLHTTP");
} catch (any_error) {
try {
request = new ActiveXObject("Microsoft.XMLHTTP");
} catch (whatever) {
request = null;
}
}
}
}
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 selector.map (function (id) {return LAPI.$ (id, doc);});
return selector.any (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 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);
result.is_gecko =
agent.contains ('gecko')
&& !/khtml|spoofer|netscape\/7\.0/.test (agent);
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.ie_version = null;
result.is_ie_le_5 = false;
result.is_ie_lt_7 = false;
if (result.is_ie) {
var version = /msie ((\d|\.)+)/.exec (agent);
result.ie_version = (version != null ? parseFloat(version[1]) : null);
result.is_ie_lt_7 = (result.ie_version != null && result.ie_version < 7);
result.is_ie_le_5 = (result.ie_version != null && result.ie_version <= 5);
}
result.is_opera = agent.contains ('opera');
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_mac = agent.contains ('mac');
return result;
})(navigator.userAgent.toLowerCase ());
} // end if (guard)
if (typeof (LAPI.IFrames) == 'undefined') {
LAPI.IFrames = {
frame_idx : 0,
handlers : {},
_callhandler : function (id)
{
if (!LAPI.IFrames.handlers[id]) return;
// Also return if the body of the iframes document is empty of is just '_': IE fires the
// onload event also when we reset or initially set the frame document.
if (window.frames) iframe = window.frames[id]; // Good idea on all IE browsers
if (!iframe) {
iframe = document.getElementsByName (id);
if (iframe && iframe.length > 0) iframe = iframe[0]; else iframe = null;
}
if (!iframe) return;
var iFrameDoc = LAPI.IFrames.IFrameDocument (iframe);
if (!iFrameDoc || !iFrameDoc.body) return;
var stuff = iFrameDoc.body.innerHTML.trim ();
if (stuff.length == 0 || stuff == '_') return;
LAPI.IFrames.handlers[id] ();
},
getIFrame : function (onload_handler)
{
var id = null;
if (LAPI.Browser.is_ie) debugger;
id = 'LAPI' + LAPI.IFrames.frame_idx++;
if (document.createElement && document.documentElement && !LAPI.Browser.is_ie) {
var styles =
{ visibility : 'hidden'
,position : 'absolute'
,width : '0px'
,height : '0px'
,borderWidth: '0px'
,left : '-1000px'
};
iframe = document.createElement ('iframe');
iframe.id = id;
iframe.name = iframe.id;
iframe.border = '0';
if (onload_handler) iframe.onload = onload_handler;
// Use visibility to hide; some browsers have problems if display='none' is used
Object.merge (styles, iframe.style);
var iframewrapper = LAPI.make ('div', {id: id + '_DIV'}, styles);
iframewrapper.appendChild (iframe);
document.body.appendChild (iframewrapper);
} else if (document.body && document.body.insertAdjacentHTML) {
// IE has problems with onload handlers...
document.body.insertAdjacentHTML (
'beforeEnd'
, '<div id="' + id + '_DIV" style="display:none;">'
+ '<iframe name="' + id + '" id="' + id + '" style="display:none;"'
+ (onload_handler ? ' onload="LAPI.IFrames._callhandler (\'' + id + '\')"' : "")
+ '></iframe>'
+ '</div>'
);
if (onload_handler) LAPI.IFrames.handlers[id] = onload_handler;
}
if (window.frames) iframe = window.frames[id]; // Good idea on all IE browsers
if (!iframe) {
iframe = document.getElementsByName (id);
if (iframe && iframe.length > 0) iframe = iframe[0]; else iframe = null;
}
if (!iframe) alert ("iframe is null");
if (iframe) {
iframe.id = id; // FF 3.0.11 somehow loses the id after insertion into the document?
if (LAPI.Browser.is_ie) {
var doc = LAPI.IFrames.IFrameDocument (iframe);
doc.open (); // Needed on IE so we have document.body set. Note: just opening and
doc.write ('_'); // closing it doesn't work, and neither does writing an empty string.
doc.close ();
}
}
return iframe;
},
loadIFrame : function (iframe, uri)
{
var iFrameDoc = LAPI.IFrames.IFrameDocument (iframe);
if ( !LAPI.Browser.is_opera
&& iFrameDoc.location && iFrameDoc.location.href != window.location.href)
iFrameDoc.location.replace (uri);
else
iframe.src = uri;
},
IFrameDocument : function (iframe)
{
return iframe.contentDocument
|| (iframe.contentWindow ? iframe.contentWindow.document : iframe.document);
},
dispose : function (iframe)
{
// Javascript-generated iframes don't have parentNode in Firefox?? Huh? According to the DOM
// specification, an HTMLIFrameElement is an Element, which is a Node, which has an attribute
// parentNode...
// LAPI.DOM.removeNode (iframe);
if (LAPI.IFrames.handlers[iframe.name]) LAPI.IFrames.handlers[iframe.name] = null;
var wrapper = LAPI.$ (iframe.name + '_DIV');
// iframe.id also may have disappeared on FF, hence use iframe.name
if (wrapper) {
// removeChild causes an exception on FF 3.0.11 if we try to remove the iframe directly:
// Could not convert JavaScript argument arg 0 [nsIDOMHTMLBodyElement.removeChild]"
// nsresult: "0x80570009 (NS_ERROR_XPC_BAD_CONVERT_JS)" location: "JS frame...
// Hence we use a div wrapper.
document.body.removeChild (wrapper);
}
}
};
} // end if (guard)
if (typeof (LAPI.Forms) == 'undefined') {
LAPI.Forms = {
encode : function (form, button)
{
var is_simple = false;
// True if it's a GET request, or if the form is 'application/x-www-form-urlencoded'
var boundary = null;
// Otherwise, it's 'multipart/form-data', and the multipart delimiter is 'boundary'
function encode_entry (name, value)
{
if (!name || name.length == 0 || !value || value.length == 0) return null;
if (!boundary)
return name + '=' + encodeURIComponent (value);
else
return boundary + '\r\n'
+ 'Content-Disposition: form-data; name="' + name + '"\r\n'
+ '\r\n'
+ value + '\r\n';
}
function encode_field (element)
{
return encode_entry (element.name || element.id, element.value);
}
function form_add_argument (args, field)
{
if (!field || field.length == 0) return args;
if (!args || args.length == 0) return field;
return args + (is_simple ? '&' : "") + field;
}
var method = form.method.toUpperCase () || 'GET';
var result = {};
result.encoding = form.enctype || 'application/x-www-form-urlencoded';
is_simple = method == 'GET' || result.encoding == 'application/x-www-form-urlencoded';
var boundary_string = '----' + wgArticleId+wgCurRevisionId + 'LAPI_submit';
if (!is_simple) boundary = '--' + boundary_string;
var args = "";
for (var i = 0; i < form.elements.length; i++) {
var element = form.elements[i];
var single_select = false;
switch (element.type) {
case 'checkbox':
case 'radio':
if (!element.checked) break;
// else fall-through
case 'hidden':
case 'text':
case 'password':
case 'textarea':
args = form_add_argument (args, encode_field (element));
break;
case 'select-one':
single_select = true;
// fall-through
case 'select-multiple':
var name = element.name || element.id || "";
if (name.length == 0) break;
for (var j = 0; j < element.length; j++) {
if (element[j].selected) {
var value = element[j].value || element[j].text;
args = form_add_argument (args, encode_entry (name, value));
if (single_select) break; // No need to scan the rest
}
}
break;
default:
break;
}
}
if (button && button.form == form)
args = form_add_argument (args, encode_field (button));
// Close the multipart data
if (!is_simple && args.length > 0) {
args = args + boundary;
result.encoding = result.encoding + '; charset=UTF-8; boundary="' + boundary_string + '"';
}
result.args = args;
return result;
}
};
} // end if (guard)
if (typeof (LAPI.HTMLRequest) == 'undefined') {
LAPI.HTMLRequest = function () { this.initialize.apply (this, arguments); };
// Wrapper for XmlHttpRequest that uses iframes if no XmlHttpRequest is
// available.
// Inspired by http://developer.apple.com/internet/webcontent/iframe.html,
// http://www.twinhelix.com/javascript/htmlhttprequest/, and
// http://ajaxpatterns.org/IFrame_Call
// This implementation differs a little in that it provides the interface
// of XmlHttpRequest, even when the actual implementation uses iframes.
LAPI.HTMLRequest.prototype =
{
initialize : function () {
this.xmlhttp = null; // LAPI.Ajax.getRequest (); // FIXME: TESTING!
if (!this.xmlhttp) {
this.readyState = 0;
this.responseText = null;
this.responseXML = null;
}
},
open : function (method, uri, callback)
{
if (this.xmlhttp) {
this.xmlhttp.open (method, uri, !!callback);
this.callback = callback;
this.xmlhttp.onreadystatechange = this._onreadystatechange.bind (this);
} else {
this.method = method;
this.contentType = null;
this.uri = uri;
this.callback = callback;
this.readyState = 1;
this.responseText = null;
this.responseXML = null;
this.iframe = LAPI.IFrames.getIFrame (this._onload.bind (this));
}
},
setRequestHeader : function (header, value)
{
if (this.xmlhttp) this.xmlhttp.setRequestHeader (header, value);
// Using IFrames, we have no way of setting request headers...
if (header.toLowerCase () == 'content-type') this.contentType = value;
},
overrideMimeType : function (mimetype)
{
if (this.xmlhttp && this.xmlhttp.overrideMimeType) this.xmlhttp.overrideMimeType (mimetype);
},
abort : function ()
{
if (this.xmlhttp) {
this.xmlhttp.abort ();
} else {
this.iframe.onload = null;
LAPI.IFrames.loadIFrame (this.iframe, "");
this._close ();
}
},
send : function (args)
{
if (this.xmlhttp) {
this.xmlhttp.send (args);
} else {
if ( args
&& this.method == 'POST'
&& this.contentType
&& this.contentType != 'application/x-www-form-urlencoded'
)
{
throw new Error ( 'HTMLRequest.send: can only send forms encoded as '
+ 'application/x-www-form-urlencoded, use submit instead');
}
if (args) this.uri = this.uri + (this.uri.contains ('?') ? '&' : '?') + args;
// Ugly hack to avoid getting back raw text as api.php, which *may* invoke
// an external program to handle the content! Note: this hack is specific to
// the MediaWiki API!
if (/[?&]format=json/.test (this.uri)) {
this.uri = this.uri.replace ('format=json', 'format=rawfm');
this.convert_rawfm = true;
} else {
this.convert_rawfm = false;
}
if (this.method == 'POST') {
var iFrameBody = LAPI.IFrames.IFrameDocument (this.iframe).body;
iFrameBody.innerHTML =
'<form method="' + this.method + '" action="' + this.uri + '"></form>';
var form = iFrameBody.firstChild;
form.submit ();
} else {
// GET request.
LAPI.IFrames.loadIFrame (this.iframe, this.uri);
}
}
},
submit : function (form, button, callback, want_result)
{
if (!this.xmlhttp && this.readyState > 0)
throw new Error ('HTMLRequest.submit: request already opened.');
var uri = form.action;
if (uri.charAt (0) == '/') uri = wgServer + uri;
var method = form.method.toUpperCase () || 'GET';
if (this.xmlhttp) {
var form_contents = LAPI.Forms.encode (form, button);
this.want_result = !!want_result;
this.user_callback = callback;
if (method == 'GET') {
uri = uri + (uri.contains ('?') ? '&' : '?') + form_contents.args;
form_contents.args = null;
}
this.open (
method
, uri
, (function (req) {
if (this.want_result) {
if (req.readyState < 4) return;
if (this.user_callback) this.user_callback (req);
this.user_callback = null;
} else {
// Call func as soon as the request has been sent and we start getting the result.
if (req.readyState == 3 && this.user_callback) {
this.user_callback (req);
this.user_callback = null;
}
}
}
).bind (this)
);
if (want_result) this.overrideMimeType ('application/xml');
if (method == 'POST') this.setRequestHeader ('Content-type', form_contents.encoding);
this.send (form_contents.args);
} else {
// I have not succeeded in making forms submit properly by retargeting them. Copying the
// whole form into an iframe didn't work either. However, the POST implementation in
// send() above works (surprise!), so we just use that. And lo and behold, it works!
var original_encoding = form.enctype;
form.enctype = 'application/x-www-form-urlencoded';
var form_contents = LAPI.Forms.encode (form, button);
if (method == 'GET') {
uri = uri + (uri.contains ('?') ? '&' : '?') + form_contents.args;
form_contents.args = null;
}
this.open (method, uri, callback);
this.overrideMimeType ('application/xml')
if (method == 'POST') this.setRequestHeader ('Content-type', form_contents.encoding);
this.send (form_contents.args);
}
},
_onload : function (evt)
{
this.status = 200;
this.statusText = 'OK';
this.readyState = 4;
this.responseXML = LAPI.IFrames.IFrameDocument (this.iframe);
if (this.convert_rawfm) {
// Hack, hack... MediaWiki API specific!
var pre = this.responseXML.getElementsByTagName ('pre');
if (pre && pre.length > 0) pre = pre[0]; else pre = null;
if (pre) this.responseText = LAPI.DOM.getInnerText (pre).decodeXML ().replace (/\t/g, "");
this.responseXML = null; // JSON results have no XML in real life!
} else {
this.responseText =
this.responseXML.innerHTML
|| (this.responseXML.documentElement ? this.responseXML.documentElement.innerHTML : null)
|| (this.responseXML.body ? this.responseXML.body.innerHTML : "");
}
if (!this.responseText) this.responseText = "";
this.responseText = this.responseText.trim ();
if (this.callback) {
try {
this.callback (this);
} catch (ex) {
this._close ();
throw ex;
}
}
this._close ();
},
_close : function ()
{
this.responseXML = null;
this.readyState = 0;
this.iframe.onload = null;
// FF also doesn't like our removing the iframe right away, it somehow doesn't close the
// connection, and leaves all busy indicators set (spinner in tab, "wait" mouse cursor).
// Therefore, just wait a little until we remove it.
window.setTimeout (this._clear.bind (this), 500); // Half a second is enough
},
_clear : function ()
{
LAPI.IFrames.dispose (this.iframe);
this.iframe = null;
},
_onreadystatechange : function ()
{
if (this.callback) this.callback (this.xmlhttp);
}
};
LAPI.HTMLRequest.submitForm = function (form, button, callback, want_result)
{
var request = new LAPI.HTMLRequest ();
request.submit (form, button, callback, want_result);
};
};
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);
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 ( ( (str.startsWith ('<!DOCTYPE html') || str.startsWith ('<html'))
&& (!doc || !doc.documentElement)
)
||
(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)
{
// Simplified from the above, for cases where we *know* up front that the text is (X)HTML.
var doc = null;
if (typeof (DOMParser) != 'undefined') {
var parser = new DOMParser ();
if (parser && parser.parseFromString)
doc = parser.parseFromString (str, 'text/xml');
}
if (!doc || !doc.documentElement || /^parsererror$/i.test (doc.documentElement.tagName)
|| (sanity_check && doc.getElementById (sanity_check) == null))
{
// We had an error, or the sanity check (looking for an element known to be there) failed.
// (Happens on Konqueror 4.2.3/4.2.4 upon the very first call...)
doc = LAPI.DOM.fakeHTMLDocument (str);
}
return doc;
},
fakeHTMLDocument : function (str)
{
var body_tag = /<body.*?>/.exec (str);
if (!body_tag || body_tag.length == 0) return null;
body_tag = body_tag.index + body_tag[0].length; // Index after the opening body tag
var body_end = str.lastIndexOf ('</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 (typeof (doc.parseError) != 'undefined') { // IE
if (doc.parseError.errorCode != 0) {
throw new Error ( 'XML parse error: ' + doc.parseError.reason
+ ' line ' + doc.parseError.line
+ ' col ' + doc.parseError.linepos
+ '\nsrc = ' + doc.parseError.srcText);
}
} else {
// FF... others?
var root = doc.documentElement;
if (/^parsererror$/i.test (root.tagName)) {
throw new Error ('XML parse error: ' + root.getInnerText ());
}
}
return doc;
},
content : function (doc)
{
// Monobook, modern, classic skins
return LAPI.$ (['bodyContent', 'mw_contentholder', 'article'], doc);
},
hasClass : function (node, className)
{
if (!node) return false;
return (' ' + node.className + ' ').contains (' ' + className + ' ');
},
setContent : function (node, content)
{
if (content == null) return node;
LAPI.DOM.removeChildren (node);
if (content.nodeName) { // presumably a DOM tree, like a span or a document fragment
node.appendChild (content);
} else if (typeof (node.innerHTML) != 'undefined') {
node.innerHTML = content.toString ();
} else {
node.appendChild (document.createTextNode (content.toString ()));
}
return node;
},
makeImage : function (src, width, height, title, doc)
{
return LAPI.make (
'img'
, {src : src, width: "" + width, height : "" + height, title : title}
, doc
);
},
makeButton : function (id, text, f, submit, doc)
{
return LAPI.make (
'input'
, {id : id || "", type: (submit ? 'submit' : 'button'), value: text, onclick: f}
, doc
);
},
makeLabel : function (id, text, for_elem, doc)
{
var label = LAPI.make ('label', {id: id || "", "for": 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 {
node.childNodes.forEach (
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 = "";
// Catch the case where the node is not yet attached in the document.
// (Or where we don't have the non-standard innerHTML.)
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;
}
}; // 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.iframe = LAPI.IFrames.getIFrame ();
this.iframedoc = LAPI.IFrames.IFrameDocument (this.iframe);
this.iframedoc.body.innerHTML = text;
this.documentElement = this.iframedoc.documentElement;
this.body = this.iframedoc.body;
this.forms = this.iframedoc.forms;
this.isFake = true;
},
dispose : function ()
{
this.iframedoc.body.InnerHTML = "";
this.documentElement = null;
this.body = null;
this.forms = null;
this.iframedoc = null;
LAPI.IFrames.dispose (this.iframe);
this.iframe = null;
},
createElement : function (tag) { return this.iframedoc.createElement (tag); },
createDocumentFragment : function () { return this.iframedoc.createDocumentFragment (); },
createTextNode : function (text) { return this.iframedoc.createTextNode (text); },
createComment : function (text) { return this.iframedoc.createComment (text); },
createCDATASection : function (text) { return this.iframedoc.createCDATASection (text); },
createAttribute : function (name) { return this.iframedoc.createAttribute (name); },
createEntityReference : function (name) { return this.iframedoc.createEntityReference (name); },
createProcessingInstruction : function (target, data) { return this.iframedoc.createProcessingInstruction (target, data); },
getElementsByTagName : function (tag) { return this.iframedoc.getElementsByTagName (tag); },
getElementById : function (id) { return this.iframedoc.getElementById (id); },
importNode : function (node, deep) { LAPI.DOM.importNode (this.iframedoc, node, deep); }
// ...NS operations omitted
}; // end DocumentFacade
} // end if (guard)
if (typeof (LAPI.Ajax.doAction) == 'undefined') {
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;
if (request.responseXML && request.responseXML.documentElement &&
(!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;
}
}
return doc;
};
// 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') return;
var original_failure = failure;
if (!failure || typeof (failure) != 'function') failure = function () {};
var request = new LAPI.HTMLRequest ();
var url = wgServer + wgScript + '?title=' + encodeURIComponent (page) + '&action=' + action;
request.open (
'GET', url
, function (req) {
if (req.readyState != 4) return;
var doc = null;
var the_form = null;
var revision_id = null;
try {
if (req.status != 200) {
throw new Error ( 'Request to server failed. Status: ' + req.status + ' '
+ 'Reason: ' + req.statusText);
}
// Convert responseText into DOM tree.
doc = LAPI.Ajax.getHTML (req, failure, form);
if (!doc) return;
if (form) {
the_form = LAPI.$ (form, doc);
if (!the_form) throw new Error ('Required form not found.');
}
revision_id = req.responseText.match (/var wgCurRevisionId = (\d+);/);
if (revision_id) revision_id = parseInt (revision_id[1]);
} catch (ex) {
failure (req, ex);
return;
}
modify (doc, the_form, original_failure, revision_id)
}
);
request.setRequestHeader ('Pragma', 'cache=yes');
request.setRequestHeader ('Cache-Control', 'no-transform');
request.overrideMimeType ('application/xml');
request.send (null);
}; // end LAPI.Ajax.doAction
LAPI.Ajax.submit = function (form, after_submit)
{
try {
LAPI.HTMLRequest.submitForm (form, null, after_submit, true);
} 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)
{
if (!failure || typeof (failure) != 'function') failure = function () {};
if (!success || typeof (success) != 'function')
throw new Error ('No success function supplied for API call '
+ action + ' with arguments ' + params);
request = new LAPI.HTMLRequest ();
var uri = wgServer + wgScriptPath + '/api.php';
if (action)
uri = uri + '?action=' + action + '&format=json';
else
uri = uri + '?format=json';
if (params != null) {
if (typeof (params) == 'string') {
uri = uri + (params.charAt (0) == '&' ? "" : '&') + params; // Must already be encoded!
} else {
for (var param in params) {
if (param != 'format') {
uri = uri + '&' + param;
if (params[param] != null) uri = uri + '=' + encodeURIComponent (params[param]);
}
}
}
}
request.open (
'GET', uri
, function (req) {
if (req.readyState != 4) return; // Wait until the request has completed.
if (req.status != 200) {
failure (req);
} else {
if (!req.responseText || req.responseText.charAt (0) != '{') {
failure (req);
} else {
success (req, eval ('(' + req.responseText + ')'));
}
}
}
);
request.setRequestHeader ('Pragma', 'cache=yes');
request.setRequestHeader ('Cache-Control', 'no-transform');
request.send (null);
}; // end LAPI.Ajax.getAPI
LAPI.Ajax.parseWikitext = function (wikitext, success, failure, as_preview, user_language, on_page)
{
if (!failure || typeof (failure) != 'function') failure = function () {};
if (!success || typeof (success) != 'function')
throw new Error ('No success function supplied for parseWikitext');
LAPI.Ajax.apiGet (
'parse'
, { 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 || wgUserLanguage) +'\}\}'
+ '\<\/div>\<div\>\n'
: "")
+ wikitext
+ (as_preview ? '\<\/div\>\<\/div\>' : "")
, title: on_page || wgPageName || "API"
, prop : 'text'
}
, 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
LAPI.Ajax.injectSpinner = injectSpinner;
LAPI.Ajax.removeSpinner = removeSpinner;
} // end if (guard)
if (typeof (LAPI.Pos) == 'undefined') {
LAPI.Pos =
{
// Returns the global coordinates of the mouse pointer within the document.
mousePosition : function (evt)
{
if (!evt || (typeof (evt.pageX) == 'undefined' && typeof (evt.clientX) == 'undefined'))
// No way to calculate a mouse pointer position
return null;
if (typeof (evt.pageX) != 'undefined')
return { x : evt.pageX, y : evt.pageY };
var offset = LAPI.Pos.scrollOffset ();
var mouse_delta = LAPI.Pos.mouse_offset ();
var coor_x = evt.clientX + offset.x - mouse_delta.x;
var coor_y = evt.clientY + offset.y - mouse_delta.y;
return { x : coor_x, y : coor_y };
},
// Operations on document level:
// Returns the scroll offset of the whole document (in other words, the coordinates
// of the top left corner of the viewport).
scrollOffset : function ()
{
return {x : LAPI.Pos.getScroll ('Left'), y : LAPI.Pos.getScroll ('Top') };
},
getScroll : function (what)
{
var s = 'scroll' + what;
return (document.documentElement ? document.documentElement[s] : 0)
|| document.body[s] || 0;
},
// Returns the size of the viewport (result.x is the width, result.y the height).
viewport : function ()
{
return {x : LAPI.Pos.getViewport ('Width'), y : LAPI.Pos.getViewport ('Height') };
},
getViewport : function (what)
{
if ( LAPI.Browser.is_opera_95 && what == 'Height'
|| LAPI.Browser.is_safari && !document.evaluate)
return window['inner' + what];
var s = 'client' + what;
if (LAPI.Browser.is_opera) return document.body[s];
return (document.documentElement ? document.documentElement[s] : 0)
|| document.body[s] || 0;
},
// Operations on DOM nodes
position : function (node)
{
var t = 0, l = 0;
do {
t = t + (node.offsetTop || 0);
l = l + (node.offsetLeft || 0);
node = node.offsetParent;
} while (node);
if (LAPI.Browser.is_mac && typeof (document.body.leftMargin) != 'undefined') {
l = l + document.body.leftMargin;
t = t + document.body.topMargin;
}
return {x : l, y : t};
},
isWithin : function (node, x, y)
{
if (!node || !node.parentNode) return false;
var pos = LAPI.Pos.position (node);
return (x == null || x >= pos.x && x < pos.x + node.offsetWidth)
&& (y == null || y >= pos.y && y < pos.y + node.offsetHeight);
},
// Private:
// IE has some strange offset...
mouse_offset : function ()
{
if (LAPI.Browser.is_ie) {
var doc_elem = document.documentElement;
if (doc_elem) {
if (typeof (doc_elem.getBoundingClientRect) == 'function') {
var tmp = doc_elem.getBoundingClientRect ();
return {x : tmp.left, y : tmp.top};
} else {
return {x : doc_elem.clientLeft, y : doc_elem.clientTop};
}
}
}
return {x: 0, y : 0};
}
}; // end LAPI.Pos
} // end if (guard)
if (typeof (LAPI.Evt) == 'undefined') {
LAPI.Evt =
{
listenTo : function (object, node, evt, f, capture)
{
var listener = LAPI.Evt.makeListener (object, f);
LAPI.Evt.attach (node, evt, listener, capture);
},
attach : function (node, evt, f, capture)
{
if (node.attachEvent) node.attachEvent ('on' + evt, f);
else if (node.addEventListener) node.addEventListener (evt, f, capture);
else node['on' + evt] = f;
},
remove : function (node, evt, f, capture)
{
if (node.detachEvent) node.detachEvent ('on' + evt, f);
else if (node.removeEventListener) node.removeEventListener (evt, f, capture);
else node['on' + evt] = null;
},
makeListener : function (obj, listener)
{
// Some hacking around to make sure 'this' is set correctly
var object = obj, f = listener;
return function (evt) { return f.apply (object, [evt || window.event]); }
// Alternative implementation:
// var f = listener.bind (obj);
// return function (evt) { return f (evt || window.event); };
},
kill : function (evt)
{
if (typeof (evt.preventDefault) == 'function') {
evt.stopPropagation ();
evt.preventDefault (); // Don't follow the link
} else if (typeof (evt.cancelBubble) != 'undefined') { // IE...
evt.cancelBubble = true;
}
return false; // Don't follow the link (IE)
}
}; // end LAPI.Evt
} // end if (guard)
if (typeof (LAPI.Edit) == 'undefined') {
LAPI.Edit = function () {this.initialize.apply (this, arguments);};
LAPI.Edit.SAVE = 1;
LAPI.Edit.PREVIEW = 2;
LAPI.Edit.REVERT = 4;
LAPI.Edit.CANCEL = 8;
LAPI.Edit.prototype =
{
initialize : function (initial_text, columns, rows, labels, handlers)
{
var my_labels =
{box : null, preview : null, save : 'Save', cancel : 'Cancel', nullsave : null, revert : null, post: null};
if (labels) my_labels = Object.merge (labels, my_labels);
this.labels = my_labels;
this.timestamp = (new Date ()).getTime ();
this.id = 'simpleedit_' + this.timestamp;
this.view = LAPI.make ('div', {id : this.id}, {marginRight: '1em'});
// Somehow, the textbox extends beyond the bounding box of the view. Don't know why, but
// adding a small margin fixes the layout more or less.
if (my_labels.box) {
var label = LAPI.make ('div');
label.appendChild (LAPI.DOM.makeLabel (null, my_labels.box));
this.view.appendChild (label);
}
this.form =
LAPI.make (
'form'
, { id : 'simple_edit_form_' + this.timestamp
,action : ""
,onsubmit: (function () {})
}
);
this.textarea =
LAPI.make (
'textarea'
, { id : 'simpleedit_text_' + this.timestamp
,cols : columns
,rows : rows
,value: (initial_text ? initial_text.toString () : "")
}
);
LAPI.Evt.attach (this.textarea, 'keyup', LAPI.Evt.makeListener (this, this.text_changed));
// Catch cut/copy/paste through the context menu. Some browsers support oncut, oncopy,
// onpaste events for this, but since that's only IE, FF 3, Safari 3, and Chrome, we
// cannot rely on this. Instead, we check again as soon as we leave the textarea. Only
// minor catch is that on FF 3, the next focus target is determined before the blur event
// fires. Since in practice save will always be enabled, this shouldn't be a problem.
LAPI.Evt.attach (this.textarea, 'mouseout', LAPI.Evt.makeListener (this, this.text_changed));
LAPI.Evt.attach (this.textarea, 'blur', LAPI.Evt.makeListener (this, this.text_changed));
this.form.appendChild (this.textarea);
this.form.appendChild (LAPI.make ('br'));
this.preview_section =
LAPI.make ('div', null, {borderBottom: '1px solid #8888aa', display: 'none'});
this.view.insertBefore (this.preview_section, this.view.firstChild);
this.save =
LAPI.DOM.makeButton
('simpleedit_save_' + this.timestamp, my_labels.save, LAPI.Evt.makeListener (this, this.do_save));
this.form.appendChild (this.save);
if (my_labels.preview) {
this.preview =
LAPI.DOM.makeButton
('simpleedit_preview_' + this.timestamp, my_labels.preview, LAPI.Evt.makeListener (this, this.do_preview));
this.form.appendChild (this.preview);
}
this.cancel =
LAPI.DOM.makeButton
('simpleedit_cancel_' + this.timestamp, my_labels.cancel, LAPI.Evt.makeListener (this, this.do_cancel));
this.form.appendChild (this.cancel);
this.view.appendChild (this.form);
if (my_labels.post) {
this.post_text = LAPI.DOM.setContent (LAPI.make ('div'), my_labels.post);
this.view.appendChild (this.post_text);
}
if (handlers) Object.merge (handlers, this);
if (typeof (this.ongettext) != 'function')
this.ongettext = function (text) { return text;}; // Default: no modifications
this.current_mask = LAPI.Edit.SAVE + LAPI.Edit.PREVIEW + + LAPI.Edit.REVERT + LAPI.Edit.CANCEL;
if ((!initial_text || initial_text.trim ().length == 0) && this.preview)
this.preview.disabled = true;
if (typeof (this.onrevert) == 'function' && my_labels.revert) {
this.revert =
LAPI.DOM.makeButton
('simpleedit_revert_' + this.timestamp, my_labels.revert, LAPI.Evt.makeListener (this, this.do_revert));
this.form.insertBefore (this.revert, this.cancel);
}
},
getView : function ()
{
return this.view;
},
getText : function ()
{
return this.ongettext (this.textarea.value);
},
setText : 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)
{
if (this.onrevert) this.setText (this.onrevert (this));
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
, wgUserLanguage || null
, wgPageName || null
);
return true;
},
enable : function (bit_set)
{
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
this.text_changed ();
}
if (this.revert) this.revert.disabled = ((bit_set & LAPI.Edit.REVERT) == 0);
},
text_changed : function (evt)
{
var text = this.textarea.value;
text = text.trim ();
var length = text.length;
if (this.preview && (this.current_mask & LAPI.Edit.PREVIEW) != 0) {
// Preview is basically enabled
this.preview.disabled = (length <= 0);
}
if (this.labels.nullsave) {
if (length > 0) {
this.save.value = this.labels.save;
} else {
this.save.value = this.labels.nullsave;
}
}
return true;
}
}; // end LAPI.Edit
} // end if (guard)
// </source>