/* ] ::: API ::: ] */
/* <pre><nowiki> */
//======================================================================
//## util/lang.js
//------------------------------------------------------------------------------
//## Object
// NOTE: these do _not_ break for (foo in bar)
/** Object helper functions */
var Objects = {
/** create an Object from a prototype */
object: function(obj) {
function F() {}
F.prototype = obj;
return new F();
},
/** copies an Object's properties into an new Object */
copyOf: function(obj) {
var out = {};
for (var key in obj)
if (obj.hasOwnProperty(key))
out = obj;
return out;
},
/** copies an object's properties into another object */
copySlots: function(source, target) {
for (var key in source)
if (source.hasOwnProperty(key))
target = source;
},
/** an object's own property names as an Array */
ownSlots: function(obj) {
var out = ;
for (key in obj)
if (obj.hasOwnProperty(key))
out.push(obj);
},
/** returns an object's slots as an Array of Pairs */
toPairs: function(obj) {
var out = ;
for (var key in obj)
if (obj.hasOwnProperty(key))
out.push(]);
return out;
},
/** creates an Object from an Array of key/value pairs, the last Pair for a key wins */
fromPairs: function(pairs) {
var out = {};
for (var i=0; i<pairs.length; i++) {
var pair = pairs;
out] = pair;
}
}//,
};
//------------------------------------------------------------------------------
//## Array
// NOTE: these _do_ break for each (foo in someArray)
/** can be used to copy a function's arguments into a real Array */
Array.make = function(args) {
return Array.prototype.slice.apply(args);
};
/** removes an element */
Array.prototype.remove = function(element) {
var index = this.indexOf(element);
if (index === -1) return false;
this.splice(index, 1);
return true;
};
/** whether this array contains an element */
Array.prototype.contains = function(element) {
return this.indexOf(element) !== -1;
};
/** flatten an Array of Arrays into a simple Array */
Array.prototype.flatten = function() {
var out = ;
this.forEach(function(element) {
out = out.concat(element);
});
return out;
};
/** map every element to an Array and concat the resulting Arrays */
Array.prototype.flatMap = function(func, thisVal) {
var out = ;
this.forEach(function(element) {
out = out.concat(func.call(thisVal, element));
});
return out;
};
/** return a new Array with a separator inserted between every element of the Array */
Array.prototype.infuse = function(separator) {
var out = ;
for (var i=0; i<this.length; i++) {
out.push(this);
out.push(separator);
}
out.pop();
return out;
};
/** use a function to extract keys and build an Object */
Array.prototype.indexWith = function(keyFunc) {
var out = {};
for (var i=0; i<this.length; i++) {
var item = this;
out = item;
}
return out;
};
//------------------------------------------------------------------------------
//## Function
/** the unary identiy function */
Function.identity = function(x) { return x; }
/** create a constant Function */
Function.constant = function(c) { return function(v) { return c; } }
/** create a Function calling this Function with a fixed this */
/*Function.prototype.bind = function(thisObject) {
var self = this; // == arguments.callee
return function() {
return self.apply(thisObject, arguments);
};
};*/
/** create a Function calling this Function with fixed first arguments */
Function.prototype.fix = function() {
var self = this; // == arguments.callee
var args = Array.prototype.slice.apply(arguments);
return function() {
return self.apply(this, args.concat(Array.prototype.slice.apply(arguments)));
};
};
//------------------------------------------------------------------------------
//## String
/** remove whitespace from both ends */
/*String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g, "");
};*/
/** return text without prefix or null */
String.prototype.scan = function(s) {
return this.substring(0, s.length) === s
? this.substring(s.length)
: null;
};
/** return text without prefix or null */
String.prototype.scanNoCase = function(s) {
return this.substring(0, s.length).toLowerCase() === s.toLowerCase()
? this.substring(s.length)
: null;
};
/** true when the string starts with the pattern */
String.prototype.startsWith = function(s) {
return this.indexOf(s) === 0;
};
/** true when the string ends in the pattern */
String.prototype.endsWith = function(s) {
return this.lastIndexOf(s) === this.length - s.length;
};
/** escapes characters to make them usable as a literal in a RegExp */
String.prototype.escapeRE = function() {
return this.replace(/(\\])/g, "\\$1");
};
/** parse a JSON String */
String.prototype.parseJSON = function() {
var text = this.replace(//g, function(a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); });
if (!/^,:{}\s]*$/.test(text.replace(/\\(?:|u{4})/g, '@').replace(/"*"|true|false|null|-?\d+(?:\.\d*)?(?:?\d+)?/g, ']').replace(/(?:^|:|,)(?:\s*\[)+/g, '')))
throw "invalid JSON string";
return eval("(" + text + ")");
};
/** replace ${name} with the name property of the args object */
String.prototype.template = function(args) {
return this.template2("${", "}", args);
};
/** replace prefix XXX suffix with the name property of the args object */
String.prototype.template2 = function(prefix, suffix, args) {
// /\$\{(+?)\}/g
var re = new RegExp(prefix.escapeRE() + "(+?)" + suffix.escapeRE(), "g");
return this.replace(re, function($0, $1) {
var arg = args;
return arg !== undefined ? arg : $0;
});
};
//------------------------------------------------------------------------------
//## Number
/** create an array of number from inclusive to exclusive */
Number.range = function(from, to) {
var out = ;
for (var i=from; i<to; i++) out.push(i);
return out;
};
//======================================================================
//## util/TextUtil.js
/** text utilities */
var TextUtil = {
/**
* gets an Array of search/replace-pairs (two Strings) and returns
* a function taking a String and replacing every search-String with
* the corresponding replace-string
*/
recoder: function(pairs) {
var search = ;
var replace = {};
for (var i=0; i<pairs.length; i++) {
var pair = pairs;
search.push(pair.escapeRE());
replace] = pair;
}
var regexp = new RegExp(search.join("|"), "gm");
return function(s) {
return s.replace(regexp, function(dollar0) {
return replace; }); };
},
/** concatenate all non-empty values in an array with a separator */
joinPrintable: function(separator, values) {
var filtered = ;
for (var i=0; i<values.length; i++) {
var value = values;
if (value === null || value === "") continue;
filtered.push(value);
}
return filtered.join(separator ? separator : "");
},
/** make a function returning its argument */
idFunc: function() {
return function(s) {
return s;
};
},
/** make a function returning a constant value */
constFunc: function(s) {
return function() {
return s;
};
},
/** make a function returning its argument */
replaceFunc: function(search, replace) {
return function(s) {
return s.replace(search, replace);
};
},
/** make a function adding a given prefix */
prefixFunc: function(separator, prefix) {
return function(suffix) {
return TextUtil.joinPrintable(separator, );
};
},
/** make a function adding a given suffix */
suffixFunc: function(separator, suffix) {
return function(prefix) {
return TextUtil.joinPrintable(separator, );
};
}//,
};
//======================================================================
//## util/XMLUtil.js
/** XML utility functions */
var XMLUtil = {
//------------------------------------------------------------------------------
//## DOM
/** parses a String into an XMLDocument */
parseXML: function(text) {
var doc = new DOMParser().parseFromString(text, "text/xml");
var root = doc.documentElement;
// root.namespaceURI === "http://www.mozilla.org/newlayout/xml/parsererror.xml"
if (root.tagName === "parserError" // ff 2
|| root.tagName === "parsererror") // ff 3
throw "XML parser error: " + root.textContent;
return doc;
},
/** serialize an XML (e4x) or XMLDocument to a String */
unparseXML: function(xml) {
return new XMLSerializer().serializeToString(xml);
},
//------------------------------------------------------------------------------
//## E4X
/** parses a String into an e4x XML object */
parseE4X: function(text) {
return new XML(text.replace(/^<\?xml*>/, ""));
},
/** serialize an XML (e4x) to a String */
unparseE4X: function(xml) {
return xml.toXMLString();
},
//------------------------------------------------------------------------------
//## escaping
/** escapes XML metacharacters */
encode: function(str) {
return str.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
},
/** escapes XML metacharacters including double quotes */
encodeDQ: function(str) {
return str.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\"/g, '"');
},
/** escapes XML metacharacters including single quotes */
encodeSQ: function(str) {
return str.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\'/g, ''');
},
/** decodes results of encode, encodeDQ and encodeSQ */
decode: function(code) {
return code.replace(/"/g, '"')
.replace(/&apos/g, "'")
.replace(/>/g, ">")
.replace(/</g, "<")
.replace(/&/g, "&");
}//,
};
//======================================================================
//## util/Loc.js
/**
* tries to behave similar to a Location object
* protocol includes everything before the //
* host is the plain hostname
* port is a number or null
* pathname includes the first slash or is null
* hash includes the leading # or is null
* search includes the leading ? or is null
*/
function Loc(urlStr) {
var m = this.parser(urlStr);
if (!m) throw "cannot parse URL: " + urlStr;
this.local = !m;
this.protocol = m ? m : null; // http:
this.host = m ? m : null; // de.wikipedia.org
this.port = m ? parseInt(m.substring(1)) : null; // 80
this.pathname = m ? m : ""; // https://wikines.com/de/Test
this.hash = m ? m : ""; // #Industry
this.search = m ? m : ""; // ?action=edit
}
Loc.prototype = {
/** matches a global or local URL */
parser: /((.+?)\/\/(+)(:+)?)?(+)?(#*)?(\?.*)?/,
/** returns the href which is the only usable string representationn of an URL */
toString: function() {
return this.hostPart() + this.pathPart();
},
/** returns everything before the pathPart */
hostPart: function() {
if (this.local) return "";
return this.protocol + "//" + this.host
+ (this.port ? ":" + this.port : "");
},
/** returns everything local to the server */
pathPart: function() {
return this.pathname + this.hash + this.search;
},
/** converts the searchstring into an Array of name/value-pairs */
args: function() {
if (!this.search) return ;
var out = ;
var split = this.search.substring(1).split("&");
for (var i=0; i<split.length; i++) {
var parts = split.split("=");
out.push([
decodeURIComponent(parts),
decodeURIComponent(parts)
]);
}
return out;
},
/** converts the searchString into a hash. */
argsMap: function() {
return Objects.fromPairs(this.args());
}//,
};
//======================================================================
//## util/DOM.js
/** DOM helper functions */
var DOM = {
//------------------------------------------------------------------------------
//## events
/** executes a function when the DOM is loaded */
onLoad: function(func) {
window.addEventListener("DOMContentLoaded", func, false);
},
//------------------------------------------------------------------------------
//## find
/** find an element in document by its id */
get: function(id) {
return document.getElementById(id);
},
/**
* find descendants of an ancestor by tagName, className and index
* tagName, className and index are optional
* returns a single element when index exists or an Array of elements if not
*/
fetch: function(ancestor, tagName, className, index) {
if (ancestor && ancestor.constructor === String) {
ancestor = document.getElementById(ancestor);
}
if (ancestor === null) return null;
var elements = ancestor.getElementsByTagName(tagName ? tagName : "*");
if (className) {
var tmp = ;
for (var i=0; i<elements.length; i++) {
if (this.hasClass(elements, className)) {
tmp.push(elements);
}
}
elements = tmp;
}
if (typeof index === "undefined") return elements;
if (index >= elements.length) return null;
return elements;
},
/** find the next element from el which has a given nodeName or is non-text */
nextElement: function(el, nodeName) {
if (nodeName) nodeName = nodeName.toUpperCase();
for (;;) {
el = el.nextSibling; if (!el) return null;
if (nodeName) { if (el.nodeName.toUpperCase() === nodeName) return el; }
else { if (el.nodeName.toUpperCase() !== "#TEXT") return el; }
}
},
/** find the previous element from el which has a given nodeName or is non-text */
previousElement: function(el, nodeName) {
if (nodeName) nodeName = nodeName.toUpperCase();
for (;;) {
el = el.previousSibling; if (!el) return null;
if (nodeName) { if (el.nodeName.toUpperCase() === nodeName) return el; }
else { if (el.nodeName.toUpperCase() !== "#TEXT") return el; }
}
},
/** whether an ancestor contains an element */
contains: function(ancestor, element) {
for (;;) {
if (element === ancestor) return true;
if (element === null) return false;
element = element.parentNode;
}
},
//------------------------------------------------------------------------------
//## add
/** inserts text, a node or an Array of these before a target node */
pasteBefore: function(target, additum) {
if (additum.constructor !== Array) additum = ;
var parent = target.parentNode;
for (var i=0; i<additum.length; i++) {
var node = additum;
if (node.constructor === String) node = document.createTextNode(node);
parent.insertBefore(node, target);
}
},
/** inserts text, a node or an Array of these after a target node */
pasteAfter: function(target, additum) {
if (target.nextSibling) this.pasteBefore(target.nextSibling, additum);
else this.pasteEnd(target.parentNode, additum);
},
/** insert text, a node or an Array of these at the start of a target node */
pasteBegin: function(parent, additum) {
if (parent.firstChild) this.pasteBefore(parent.firstChild, additum);
else this.pasteEnd(parent, additum);
},
/** insert text, a node or an Array of these at the end of a target node */
pasteEnd: function(parent, additum) {
if (additum.constructor !== Array) additum = ;
for (var i=0; i<additum.length; i++) {
var node = additum;
if (node.constructor === String) node = document.createTextNode(node);
parent.appendChild(node);
}
},
//------------------------------------------------------------------------------
//## remove
/** remove a node from its parent node */
removeNode: function(node) {
node.parentNode.removeChild(node);
},
/** removes all children of a node */
removeChildren: function(node) {
//while (node.lastChild) node.removeChild(node.lastChild);
node.innerHTML = "";
},
//------------------------------------------------------------------------------
//## replace
/** replace a node with another one */
replaceNode: function(node, replacement) {
node.parentNode.replaceChild(replacement, node);
},
//------------------------------------------------------------------------------
//## css classes
/** creates a RegExp matching a className */
classNameRE: function(className) {
return new RegExp("(^|\\s+)" + className.escapeRE() + "(\\s+|$)");
},
/** returns an Array of the classes of an element */
getClasses: function(element) {
return element.className.split(/\s+/);
},
/** returns whether an element has a class */
hasClass: function(element, className) {
if (!element.className) return false;
var re = this.classNameRE(className);
return re.test(element.className);
// return (" " + element.className + " ").indexOf(" " + className + " ") !== -1;
},
/** adds a class to an element */
addClass: function(element, className) {
if (this.hasClass(element, className)) return;
var old = element.className ? element.className : "";
element.className = (old + " " + className).trim();
},
/** removes a class to an element */
removeClass: function(element, className) {
var re = this.classNameRE(className);
var old = element.className ? element.className : "";
element.className = old.replace(re, "");
},
/** replaces a class in an element with another */
replaceClass: function(element, oldClassName, newClassName) {
this.removeClass(element, oldClassName);
this.addClass(element, newClassName);
},
/** sets or unsets a class on an element */
updateClass: function(element, className, active) {
var has = this.hasClass(element, className);
if (has === active) return;
if (active) this.addClass(element, className);
else this.removeClass(element, className);
},
//------------------------------------------------------------------------------
//## position
/** mouse position in document base coordinates */
mousePos: function(event) {
return {
x: window.pageXOffset + event.clientX,
y: window.pageYOffset + event.clientY
};
},
/** minimum visible position in document base coordinates */
minPos: function() {
return {
x: window.scrollX,
y: window.scrollY
};
},
/** maximum visible position in document base coordinates */
maxPos: function() {
return {
x: window.scrollX + window.innerWidth,
y: window.scrollY + window.innerHeight
};
},
/** position of an element in document base coordinates */
elementPos: function(element) {
var parent = this.elementParentPos(element);
return {
x: element.offsetLeft + parent.x,
y: element.offsetTop + parent.y
};
},
/** size of an element */
elementSize: function(element) {
return {
x: element.offsetWidth,
y: element.offsetHeight
};
},
/** document base coordinates for an elements parent */
elementParentPos: function(element) {
// TODO inline in elementPos?
var pos = { x: 0, y: 0 };
for (;;) {
var mode = window.getComputedStyle(element, null).position;
if (mode === "fixed") {
pos.x += window.pageXOffset;
pos.y += window.pageYOffset;
return pos;
}
var parent = element.offsetParent;
if (!parent) return pos;
pos.x += parent.offsetLeft;
pos.y += parent.offsetTop;
// TODO add scrollTop and scrollLeft here?
element = parent;
}
},
/** moves an element to document base coordinates */
moveElement: function(element, pos) {
var container = this.elementParentPos(element);
element.style.left = (pos.x - container.x) + "px";
element.style.top = (pos.y - container.y) + "px";
}//,
};
//======================================================================
//## util/Cookie.js
/** helper functions for cookies */
var Cookie = {
TTL_DEFAULT: 1*31*24*60*60*1000, // in a month
TTL_DELETE: -3*24*60*60*1000, // 3 days before
/** get a named cookie or returns null */
get: function(key) {
var point = new RegExp("\\b" + encodeURIComponent(key).escapeRE() + "=");
var s = document.cookie.split(point);
if (!s) return null;
s = s.split(";").replace(/ *$/, "");
return decodeURIComponent(s);
},
/** set a named cookie */
set: function(key, value, expires) {
if (!expires) expires = this.timeout(this.TTL_DEFAULT);
document.cookie = encodeURIComponent(key) + "=" + encodeURIComponent(value) +
"; expires=" + expires.toGMTString() +
"; path=/";
},
/** delete a named cookie */
del: function(key) {
this.set(key, "",
this.timeout(this.TTL_DELETE));
},
/** calculate a date a given number of millis in the future */
timeout: function(offset) {
var expires = new Date();
expires.setTime(expires.getTime() + offset);
return expires;
}//,
};
//======================================================================
//## util/Ajax.js
/** ajax helper */
var Ajax = {
/**
* create and use an XMLHttpRequest with named parameters
*
* data
* method optional string, defaults to GET
* url mandatory string, may contains parameters
* urlParams optional map or Array of pairs, can be used together with params in url
* body optional string
* bodyParams optional map or Array of pairs, overwrites body
* charset optional string for bodyParams
* headers optional map
* timeout optional number of milliseconds
*
* callbacks, all get the client as first parameter
* exceptionFunc called when the client throws an exception
* completeFunc called after the more specific functions
* noSuccessFunc called in all cases when no success
*
* successFunc called for 200..300, gets the responseText
* intermediateFunc called for 300..400
* failureFunc called for 400..500
* errorFunc called for 500..600
*/
call: function(args) {
if (!args.url) throw "url argument missing";
// create client
var client = new XMLHttpRequest();
client.args = args;
client.debug = function() {
return client.status + " " + client.statusText + "\n"
+ client.getAllResponseHeaders() + "\n\n"
+ client.responseText;
};
// init client
var method = args.method || "GET";
var url = args.url;
if (args.urlParams) {
url += url.indexOf("?") === -1 ? "?" : "&";
url += this.encodeUrlArgs(args.urlParams);
}
client.open(method, url, true);
// state callback
client.onreadystatechange = function() {
if (client.readyState !== 4) return;
if (client.timer) clearTimeout(client.timer);
var status = -1;
try { status = client.status; }
catch (e) {
if (args.exceptionFunc) args.exceptionFunc(client, e);
if (args.noSuccessFunc) args.noSuccessFunc(client, e);
return;
}
if (status >= 200 && status < 300) {
if (args.successFunc) args.successFunc(client, client.responseText);
}
else if (status >= 300 && status < 400) {
// TODO location-header?
if (args.intermediateFunc) args.intermediateFunc(client);
}
else if (status >= 400 && status < 500) {
if (args.failureFunc) args.failureFunc(client);
}
else if (status >= 500 && status < 600) {
if (args.errorFunc) args.errorFunc(client);
}
if (args.completeFunc) args.completeFunc(client);
if (status < 200 || status >= 300) {
if (args.noSuccessFunc) args.noSuccessFunc(client);
}
};
// init headers
if (args.bodyParams) {
var contentType = "application/x-www-form-urlencoded";
if (args.charset) contentType += "; charset=" + args.charset;
client.setRequestHeader("Content-Type", contentType);
}
if (args.headers) {
for (var key in args.headers) {
client.setRequestHeader(key, args.headers);
}
}
// init body
var body;
if (args.bodyParams) {
body = this.encodeFormArgs(args.bodyParams);
}
else {
body = args.body || null;
}
// send
if (args.timeout) {
client.timer = setTimeout(client.abort.bind(client), args.timeout);
}
client.send(body);
},
//------------------------------------------------------------------------------
//## private
/**
* url-encode arguments
* args may be an Array of Pair of String or a Map from String to String
*/
encodeUrlArgs: function(args) {
if (args.constructor !== Array) args = this.hashToPairs(args);
return this.encodeArgPairs(args, encodeURIComponent);
},
/**
* encode arguments into application/x-www-form-urlencoded
* args may be an Array of Pair of String or a Map from String to String
*/
encodeFormArgs: function(args) {
if (args.constructor !== Array) args = this.hashToPairs(args);
return this.encodeArgPairs(args, this.encodeFormValue);
},
/** compile an Array of Pairs of Strings into the &name=value format */
encodeArgPairs: function(args, encodeFunc) {
var out = "";
for (var i=0; i<args.length; i++) {
var pair = args;
if (pair.constructor !== Array) throw "expected a Pair: " + pair;
if (pair.length !== 2) throw "expected a Pair: " + pair;
if (pair === null) continue;
out += "&" + encodeFunc(pair)
+ "=" + encodeFunc(pair);
}
return out && out.substring(1);
},
/** encode a single form-value. this is a variation on url-encoding */
encodeFormValue: function(value) {
// use windows-linefeeds
value = value.replace(/\r\n|\n|\r/g, "\r\n");
// escape funny characters
value = encodeURIComponent(value);
// space is encoded as a plus sign instead of "%20"
value = value.replace(/(^|)(%%)*%20/g, "$1$2+");
return value;
},
/**
* converts a hash into an Array of Pairs (2-element Arrays).
* null values generate no Pair,
* array values generate multiple Pairs,
* other values are toString()ed
*/
hashToPairs: function(map) {
var out = ;
for (var key in map) {
var value = map;
if (value === null) continue;
if (value.constructor === Array) {
for (var i=0; i<value.length;i++) {
var subValue = value;
if (subValue === null) continue;
out.push();
}
continue;
}
out.push();
}
return out;
}//,
};
//======================================================================
//## util/Form.js
/** HTMLFormElement helper functions */
var Form = {
//------------------------------------------------------------------------------
///## finder
/** finds a HTMLForm or returns null */
find: function(ancestor, nameOrIdOrIndex) {
var forms = ancestor.getElementsByTagName("form");
if (typeof nameOrIdOrIndex === "number") {
if (nameOrIdOrIndex >= 0
&& nameOrIdOrIndex < forms.length) return forms;
else return null;
}
for (var i=0; i<forms.length; i++) {
var form = forms;
if (this.elementNameOrId(form) === nameOrIdOrIndex) return form;
}
return null;
},
/** returns the name or id of an element or null */
elementNameOrId: function(element) {
return element.name ? element.name
: element.id ? element.id
: null;
},
//------------------------------------------------------------------------------
//## serializer
/**
* parses HTMLFormElement and its HTML*Element children
* into an Array of name/value-pairs (2-element Arrays).
* these pairs can be used as bodyArgs parameter for Ajax.call.
*
* returns an Array of Pairs, optionally with one of
* the button/image/submit-elements activated
*/
serialize: function(form, buttonName) {
var out = ;
for (var i=0; i<form.elements.length; i++) {
var element = form.elements;
if (!element.name) continue;
if (element.disabled) continue;
var handlingButton = element.type === "submit"
|| element.type === "image"
|| element.type === "button";
if (handlingButton
&& element.name !== buttonName) continue;
var pairs = this.elementPairs(element);
out = out.concat(pairs);
}
return out;
},
/**
* returns an Array of Pairs for a single input element.
* in most cases, it contains zero or one Pair.
* more than one are possible for select-multiple.
*/
elementPairs: function(element) {
var name = element.name;
var type = element.type;
var value = element.value;
if (type === "reset") {
return ;
}
else if (type === "submit") {
if (value) return ];
else return ];
}
else if (type === "button" || type === "image") {
if (value) return ];
else return ];
}
else if (type === "checkbox" || type === "radio") {
if (!element.checked) return ;
else if (value !== null) return ];
else return ];
}
else if (type === "select-one") {
if (element.selectedIndex !== -1) return ];
else return ;
}
else if (type === "select-multiple") {
var pairs = ;
for (var i=0; i<element.options.length; i++) {
var opt = element.options;
if (!opt.selected) continue;
pairs.push();
}
return pairs;
}
else if (type === "text" || type === "password" || type === "hidden" || type === "textarea") {
if (value) return ];
else return ];
}
else if (type === "file") {
// NOTE: can't do anything here :(
return ;
}
else {
throw "field: " + name + " has the unknown type: " + type;
}
}//,
};
//======================================================================
//## lib/core/Actions.js
/**
* ajax functions for MediaWiki
* uses wgScript, wgScriptPath and Titles.specialPage
*/
var Actions = {
/**
* example feedback implementation, implement this interface
* if you want to get notified about an Actions progress
*/
NoFeedback: {
job: function(s) {},
work: function(s) {},
success: function(s) {},
failure: function(s) {}//,
},
//------------------------------------------------------------------------------
//## change page content
/** replace the text of a page with a replaceFunc. the replaceFunc can return null to abort. */
replaceText: function(feedback, title, replaceFunc, summary, minorEdit, allowCreate, doneFunc) {
this.editPage(feedback, title, null, null, summary, minorEdit, allowCreate, replaceFunc, doneFunc);
},
/** add text to the end of a spage, the separator is optional */
appendText: function(feedback, title, text, summary, separator, allowCreate, doneFunc) {
var changeFunc = TextUtil.suffixFunc(separator, text);
this.editPage(feedback, title, null, null, summary, false, allowCreate, changeFunc, doneFunc);
},
/** add text to the start of a page, the separator is optional */
prependText: function(feedback, title, text, summary, separator, allowCreate, doneFunc) {
// could use section=0 if there wasn't the separator
var changeFunc = TextUtil.prefixFunc(separator, text);
this.editPage(feedback, title, null, null, summary, false, allowCreate, changeFunc, doneFunc);
},
/** restores a page to an older version */
restoreVersion: function(feedback, title, oldid, summary, doneFunc) {
var changeFunc = TextUtil.idFunc();
this.editPage(feedback, title, oldid, null, summary, false, false, changeFunc, doneFunc);
},
/**
* edits a page's text
* except feedback and title, all parameters can be null
*/
editPage: function(feedback, title, oldid, section, summary, minor, allowCreate, textFunc, doneFunc) {
feedback = feedback || this.NoFeedback;
feedback.job("editing page: " + title + " with oldid: " + oldid + " and section: " + section);
var args = {
title: title,
oldid: oldid,
section: section,
action: "edit"
};
var self = this;
function change(form, doc) {
if (!allowCreate && doc.getElementById("newarticletext")) return false;
if (summary !== null) form.elements.value = summary;
if (minor !== null) form.elements.checked = minor;
var text = form.elements.value;
if (textFunc) {
text = text.replace(/^+$/, "");
text = textFunc(text);
if (text === null) { feedback.failure("aborted"); return false; }
}
form.elements.value = text
return true;
}
var afterEdit = this.afterEditFunc(feedback, doneFunc);
this.action(feedback, args, "editform", change, 200, afterEdit);
},
/** undoes an edit */
undoVersion: function(feedback, title, undo, undoafter, doneFunc) {
feedback = feedback || this.NoFeedback;
feedback.job("undoing page: " + title + " with undo: " + undo + " and undoafter: " + undoafter);
var args = {
title: title,
undo: undo,
undoafter: undoafter,
action: "edit"
};
function change(form, doc) {
return true;
}
var afterEdit = this.afterEditFunc(feedback, doneFunc);
this.action(feedback, args, "editform", change, 200, afterEdit);
},
/**
* finds the newest edits of a user on a page
* the foundFunc is called with title, user, previousUser, revId and timestamp
*/
newEdits: function(feedback, title, user, foundFunc) {
feedback = feedback || this.NoFeedback;
var self = this;
function phase1() {
feedback.work("fetching revision history");
var apiArgs = {
action: "query",
prop: "revisions",
titles: title,
rvprop: "ids|timestamp|flags|comment|user",
rvlimit: 50//,
};
self.apiCall(feedback, apiArgs, phase2);
}
function phase2(json) {
feedback.work("parsing revision history");
/** returns the first element of a map */
function firstElement(map) {
for (key in map) { return map; }
return null;
}
var page = firstElement(json.query.pages);
if (page == null) {
feedback.failure("no suitable revision found");
return;
}
var rev = (function() {
var revs = page.revisions;
for (var i=0; i<revs.length; i++) {
var rev = revs;
rev.index = i;
if (rev.user !== user) return rev;
}
return null; // no version found;
})();
if (rev === null) {
feedback.failure("no suitable revision found");
return;
}
if (rev.index === 0) {
feedback.failure("found conflicting revision by user " + rev.user);
return;
}
feedback.success("found revision " + rev.revid);
foundFunc(title, user, rev.user, rev.revid, rev.timestamp);
}
phase1();
},
//------------------------------------------------------------------------------
//## change page state
/** watch or unwatch a page. the doneFunc is optional */
watchedPage: function(feedback, title, watch, doneFunc) {
feedback = feedback || this.NoFeedback;
var action = watch ? "watch" : "unwatch";
feedback.job(action + " page: " + title);
var actionArgs = {
title: title,
action: action//,
};
feedback.work("GET " + mw.config.get('wgScript') + " with " + this.debugArgsString(actionArgs));
function done(source) {
if (source.status !== 200) {
// source.args.method, source.args.url
feedback.failure(source.status + " " + source.statusText);
return;
}
feedback.success("done");
if (doneFunc) doneFunc();
}
Ajax.call({
method: "GET",
url: mw.config.get('wgScript'),
urlParams: actionArgs,
successFunc: done//,
});
},
/** move a page */
movePage: function(feedback, oldTitle, newTitle, reason, moveTalk, moveSub, watch, leaveRedirect, doneFunc) {
feedback = feedback || this.NoFeedback;
feedback.job("move page: " + oldTitle + " to: " + newTitle);
var args = {
title: this.specialTitle("MovePage"),
target: oldTitle // url-encoded, mandatory
};
function change(form, doc) {
form.elements.value = oldTitle;
form.elements.value = newTitle;
form.elements.value = reason;
if (form.elements)
form.elements.checked = moveTalk;
if (form.elements)
form.elements.checked = moveSub;
if (form.elements)
form.elements.checked = leaveRedirect;
form.elements.value = watch;
// TODO wpConfirm
return true;
}
this.action(feedback, args, "movepage", change, 200, doneFunc);
},
/** rollback an edit, the summary may be null */
rollbackEdit: function(feedback, title, from, token, summary, doneFunc) {
feedback = feedback || this.NoFeedback;
feedback.job("rolling back page: " + title + " from: " + from);
var actionArgs = {
title: title,
from: from,
token: token,
summary: summary,
action: "rollback"//,
};
feedback.work("GET " + mw.config.get('wgScript') + " with " + this.debugArgsString(actionArgs));
function done(source) {
if (source.status !== 200) {
// source.args.method, source.args.url
feedback.failure(source.status + " " + source.statusText);
return;
}
feedback.success("done");
if (doneFunc) doneFunc();
}
Ajax.call({
method: "GET",
url: mw.config.get('wgScript'),
urlParams: actionArgs,
successFunc: done//,
});
},
/** delete a page. if the reason is null, the original reason text is deleted */
deletePage: function(feedback, title, reason, doneFunc) {
feedback = feedback || this.NoFeedback;
feedback.job("delete page: " + title);
var args = {
title: title,
action: "delete"
};
function change(form, doc) {
if (reason !== null) {
reason = TextUtil.joinPrintable(" - ", [
reason,
form.elements.value ]);
}
else {
reason = "";
}
form.elements.value = reason;
return true;
}
this.action(feedback, args, "deleteconfirm", change, 200, doneFunc);
},
/** delete a file. if the reason is null, the original reason text is deleted */
deleteFile: function(feedback, title, reason, doneFunc) {
feedback = feedback || this.NoFeedback;
feedback.job("delete file: " + title);
var args = {
title: title,
action: "delete"
};
function change(form, doc) {
if (reason !== null) {
reason = TextUtil.joinPrintable(" - ", [
reason,
form.elements.value ]);
}
else {
reason = "";
}
form.elements.value = reason;
// mw-filedelete-submit
return true;
}
this.action(feedback, args, 0, change, 200, doneFunc);
},
/**
* change a page's protection state
* allowed values for the levels are "", "autoconfirmed" and "sysop"
* cascade should be false in most cases
* expiry may be empty for indefinite, "indefinite",
* or a number followed by a space and
* "years", "months", "days", "hours" or "minutes"
*/
protectPage: function(feedback, title,
levelEdit, expiryEdit,
levelMove, expiryMove,
levelCreate, expiryCreate,
reason, cascade, watch, doneFunc) {
feedback = feedback || this.NoFeedback;
feedback.job("protect page: " + title);
var args = {
title: title,
action: "protect"
};
function change(form, doc) {
// for existing pages
if (form.elements)
form.elements.value = levelEdit; // plus mwProtectExpirySelection-edit named wpProtectExpirySelection-edit
if (form.elements)
form.elements.value = expiryEdit; // named mwProtect-expiry-edit
// for existing pages
if (form.elements)
form.elements.value = levelMove; // plus mwProtectExpirySelection-move named wpProtectExpirySelection-move
if(form.elements)
form.elements.value = expiryMove; // named mwProtect-expiry-move
// for deleted pages
if (form.elements)
form.elements.value = levelCreate; // plus mwProtectExpirySelection-create named wpProtectExpirySelection-create
if (form.elements)
form.elements.value = expiryMove; // named mwProtect-expiry-create
// for both deleted and existing pages
form.elements.checked = cascade;
form.elements.value = reason; // plus wpProtectReasonSelection
form.elements.value = watch;
return true;
}
// this form does not have a name
this.action(feedback, args, 0, change, 200, doneFunc);
},
//------------------------------------------------------------------------------
//## change other data
/**
* block a user.
* anonOnly, createAccounts, enableAutoblock and allowUserTalk default to true
* expiry may be "indefinite",
* or a number followed by a space and
* "years", "months", "days", "hours" or "minutes"
*/
blockUser: function(feedback, user, expiry, reason, anonOnly, createAccount, enableAutoblock, emailBan, allowUserTalk, watchUser, allowChange, doneFunc) {
feedback = feedback || this.NoFeedback;
feedback.job("block user: " + user + " for: " + expiry);
var args = {
title: this.specialTitle("BlockIP"),
ip: user // url-encoded, optional
};
function change(form, doc) {
if (!allowChange && form.elements) return false;
form.elements.value = user;
form.elements.value = reason;
form.elements.checked = anonOnly;
form.elements.checked = createAccount;
form.elements.checked = enableAutoblock;
form.elements.checked = emailBan;
form.elements.checked = allowUserTalk;
form.elements.checked = watchUser;
form.elements.value = expiry;
return true;
}
this.action(feedback, args, "blockip", change, 200, doneFunc);
},
/** send an email to a user. */
sendEmail: function(feedback, user, subject, body, ccSelf, doneFunc) {
feedback = feedback || this.NoFeedback;
feedback.job("sending email to user: " + user + " with subject: " + subject);
var args = {
title: this.specialTitle("EmailUser"),
target: user
};
function change(form, doc) {
form.elements.value = subject;
form.elements.value = body;
form.elements.value = ccSelf;
return true;
}
this.action(feedback, args, "emailuser", change, 200, doneFunc);
},
//------------------------------------------------------------------------------
//## private
/** returns a doneFunc displaying an error if an edit was not successful or calls the (optional) doneFunc */
afterEditFunc: function(feedback, doneFunc) {
feedback = feedback || this.NoFeedback;
return function(text) {
var doc;
try {
doc = XMLUtil.parseXML(text);
}
catch (e) {
feedback.failure("cannot parse XML: " + e);
return;
}
if (doc.getElementById('wikiPreview')) {
feedback.failure("cannot save, preview detected");
return;
}
var form = Form.find(doc, "editform");
if (form) {
feedback.failure("cannot save, editform detected");
return;
}
if (doneFunc) doneFunc(text);
};
},
/**
* get a form, change it, post it.
* the changeFunc gets the form as its first, the complete document as its second parameter
* and modifies this form in-place. it may return false to abort.
* the doneFunc is called after modification with the document text and may be left out
*/
action: function(feedback, actionArgs, formName, changeFunc, expectedPostStatus, doneFunc) {
feedback = feedback || this.NoFeedback;
var self = this;
function phase1() {
// get the form
feedback.work("GET " + mw.config.get('wgScript') + " with " + self.debugArgsString(actionArgs));
Ajax.call({
method: "GET",
url: mw.config.get('wgScript'),
urlParams: actionArgs,
successFunc: phase2,
noSuccessFunc: failure//,
});
}
function phase2(source) {
// check status
var expectedGetStatus = 200;
if (expectedGetStatus && source.status !== expectedGetStatus) {
feedback.failure(source.status + " " + source.statusText);
return;
}
// get document
var doc;
try {
doc = XMLUtil.parseXML(source.responseText);
}
catch (e) {
feedback.failure("cannot parse XML: " + e);
return;
}
// get form
var form = Form.find(doc, formName);
if (form === null) {
feedback.failure("missing form: " + formName);
return;
}
// modify form
var ok;
try {
ok = changeFunc(form, doc);
}
catch(e) {
feedback.failure("cannot change form: " + e);
return;
}
if (!ok) {
feedback.failure("aborted");
return;
}
// post the form
var url = form.action;
var data = Form.serialize(form);
feedback.work("POST " + url);
Ajax.call({
method: "POST",
url: url,
bodyParams: data,
successFunc: phase3,
noSuccessFunc: failure//,
});
}
function phase3(source) {
// check status
if (expectedPostStatus && source.status !== expectedPostStatus) {
feedback.failure(source.status + " " + source.statusText);
return;
}
// done
feedback.success("done");
if (doneFunc) doneFunc(source.responseText);
}
function failure(source) {
feedback.failure(source.status + ": " + self.debugArgsString(source.args));
}
phase1();
},
/** call the api, calls doneFunc with the JSON result (and the response text) if successful */
apiCall: function(feedback, args, doneFunc) {
feedback = feedback || this.NoFeedback;
//var self = this;
function phase1() {
var apiPath = mw.config.get('wgScriptPath') + "/api.php";
var bodyParams = {
format: "json"//,
};
Objects.copySlots(args, bodyParams);
feedback.work("POST " + apiPath);
Ajax.call({
url: apiPath,
method: "POST",
bodyParams: bodyParams,
successFunc: phase2,
noSuccessFunc: failure//,
});
}
function phase2(source) {
var text = source.responseText;
var json;
try {
json = text.parseJSON();
}
catch (e) {
feedback.failure("cannot parse JSON: " + e);
return;
}
feedback.success("done");
if (doneFunc) doneFunc(json, text);
}
function failure(source) {
feedback.failure("api status: " + source.status);
}
phase1();
},
/** bring a map into a human readable form */
debugArgsString: function(args) {
var out = "";
for (key in args) {
var arg = args;
if (arg !== null && arg.constructor === Function) continue;
out += ", " + key + ": " + arg;
}
return out.substring(2);
},
/** SpecialPage access, uses Titles.specialPage if Titles exists */
specialTitle: function(specialName) {
// HACK for standalone operation
if (!window.Titles) return "Special:" + specialName;
return Titles.specialPage(specialName);
}//,
};
/* </nowiki></pre> */